mirror of https://github.com/Squidex/squidex.git
Browse Source
* Comment trigger. * Fixes. * More backend work. * User mentioned trigger finished. * More tests. * Alternative top query parameter.pull/470/head
committed by
GitHub
85 changed files with 887 additions and 191 deletions
@ -0,0 +1,28 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Runtime.Serialization; |
|||
using Squidex.Shared.Users; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents |
|||
{ |
|||
public sealed class EnrichedCommentEvent : EnrichedUserEventBase |
|||
{ |
|||
public string Text { get; set; } |
|||
|
|||
public Uri? Url { get; set; } |
|||
|
|||
[IgnoreDataMember] |
|||
public IUser MentionedUser { get; set; } |
|||
|
|||
public override long Partition |
|||
{ |
|||
get { return MentionedUser.Id.GetHashCode(); } |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure.Reflection; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Rules.Triggers |
|||
{ |
|||
[TypeName(nameof(CommentTrigger))] |
|||
public sealed class CommentTrigger : RuleTrigger |
|||
{ |
|||
public string Condition { get; set; } |
|||
|
|||
public override T Accept<T>(IRuleTriggerVisitor<T> visitor) |
|||
{ |
|||
return visitor.Visit(this); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,77 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Squidex.Domain.Apps.Core.HandleRules; |
|||
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
|||
using Squidex.Domain.Apps.Core.Rules.Triggers; |
|||
using Squidex.Domain.Apps.Core.Scripting; |
|||
using Squidex.Domain.Apps.Events; |
|||
using Squidex.Domain.Apps.Events.Comments; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.EventSourcing; |
|||
using Squidex.Infrastructure.Reflection; |
|||
using Squidex.Shared.Users; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Comments |
|||
{ |
|||
public sealed class CommentTriggerHandler : RuleTriggerHandler<CommentTrigger, CommentCreated, EnrichedCommentEvent> |
|||
{ |
|||
private static readonly List<EnrichedEvent> EmptyResult = new List<EnrichedEvent>(); |
|||
private readonly IScriptEngine scriptEngine; |
|||
private readonly IUserResolver userResolver; |
|||
|
|||
public CommentTriggerHandler(IScriptEngine scriptEngine, IUserResolver userResolver) |
|||
{ |
|||
Guard.NotNull(scriptEngine); |
|||
Guard.NotNull(userResolver); |
|||
|
|||
this.scriptEngine = scriptEngine; |
|||
|
|||
this.userResolver = userResolver; |
|||
} |
|||
|
|||
public override async Task<List<EnrichedEvent>> CreateEnrichedEventsAsync(Envelope<AppEvent> @event) |
|||
{ |
|||
var commentCreated = @event.Payload as CommentCreated; |
|||
|
|||
if (commentCreated?.Mentions?.Length > 0) |
|||
{ |
|||
var users = await userResolver.QueryManyAsync(commentCreated.Mentions); |
|||
|
|||
if (users.Count > 0) |
|||
{ |
|||
var result = new List<EnrichedEvent>(); |
|||
|
|||
foreach (var user in users.Values) |
|||
{ |
|||
var enrichedEvent = new EnrichedCommentEvent |
|||
{ |
|||
MentionedUser = user |
|||
}; |
|||
|
|||
enrichedEvent.Name = "UserMentioned"; |
|||
|
|||
SimpleMapper.Map(commentCreated, enrichedEvent); |
|||
|
|||
result.Add(enrichedEvent); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
} |
|||
|
|||
return EmptyResult; |
|||
} |
|||
|
|||
protected override bool Trigger(EnrichedCommentEvent @event, CommentTrigger trigger) |
|||
{ |
|||
return string.IsNullOrWhiteSpace(trigger.Condition) || scriptEngine.Evaluate("event", @event, trigger.Condition); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Domain.Apps.Core.Rules; |
|||
using Squidex.Domain.Apps.Core.Rules.Triggers; |
|||
using Squidex.Infrastructure.Reflection; |
|||
|
|||
namespace Squidex.Areas.Api.Controllers.Rules.Models.Triggers |
|||
{ |
|||
public class CommentRuleTriggerDto : RuleTriggerDto |
|||
{ |
|||
/// <summary>
|
|||
/// Javascript condition when to trigger.
|
|||
/// </summary>
|
|||
public string? Condition { get; set; } |
|||
|
|||
public override RuleTrigger ToTrigger() |
|||
{ |
|||
return SimpleMapper.Map(this, new CommentTrigger()); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,293 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using FakeItEasy; |
|||
using Squidex.Domain.Apps.Core.HandleRules; |
|||
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
|||
using Squidex.Domain.Apps.Core.Rules.Triggers; |
|||
using Squidex.Domain.Apps.Core.Scripting; |
|||
using Squidex.Domain.Apps.Events; |
|||
using Squidex.Domain.Apps.Events.Comments; |
|||
using Squidex.Domain.Apps.Events.Contents; |
|||
using Squidex.Infrastructure.EventSourcing; |
|||
using Squidex.Shared.Users; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Comments |
|||
{ |
|||
public class CommentTriggerHandlerTests |
|||
{ |
|||
private readonly IScriptEngine scriptEngine = A.Fake<IScriptEngine>(); |
|||
private readonly IUserResolver userResolver = A.Fake<IUserResolver>(); |
|||
private readonly IRuleTriggerHandler sut; |
|||
|
|||
public CommentTriggerHandlerTests() |
|||
{ |
|||
A.CallTo(() => scriptEngine.Evaluate("event", A<object>.Ignored, "true")) |
|||
.Returns(true); |
|||
|
|||
A.CallTo(() => scriptEngine.Evaluate("event", A<object>.Ignored, "false")) |
|||
.Returns(false); |
|||
|
|||
sut = new CommentTriggerHandler(scriptEngine, userResolver); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_create_enriched_events() |
|||
{ |
|||
var user1 = CreateUser("1"); |
|||
var user2 = CreateUser("2"); |
|||
|
|||
var users = new List<IUser> { user1, user2 }; |
|||
var userIds = users.Select(x => x.Id).ToArray(); |
|||
|
|||
var envelope = Envelope.Create<AppEvent>(new CommentCreated { Mentions = userIds }); |
|||
|
|||
A.CallTo(() => userResolver.QueryManyAsync(userIds)) |
|||
.Returns(users.ToDictionary(x => x.Id)); |
|||
|
|||
var result = await sut.CreateEnrichedEventsAsync(envelope); |
|||
|
|||
Assert.Equal(2, result.Count); |
|||
|
|||
var enrichedEvent1 = result[0] as EnrichedCommentEvent; |
|||
var enrichedEvent2 = result[1] as EnrichedCommentEvent; |
|||
|
|||
Assert.Equal(user1, enrichedEvent1!.MentionedUser); |
|||
Assert.Equal(user2, enrichedEvent2!.MentionedUser); |
|||
Assert.Equal("UserMentioned", enrichedEvent1.Name); |
|||
Assert.Equal("UserMentioned", enrichedEvent2.Name); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_not_create_enriched_events_when_users_cannot_be_resolved() |
|||
{ |
|||
var user1 = CreateUser("1"); |
|||
var user2 = CreateUser("2"); |
|||
|
|||
var users = new List<IUser> { user1, user2 }; |
|||
var userIds = users.Select(x => x.Id).ToArray(); |
|||
|
|||
var envelope = Envelope.Create<AppEvent>(new CommentCreated { Mentions = userIds }); |
|||
|
|||
var result = await sut.CreateEnrichedEventsAsync(envelope); |
|||
|
|||
Assert.Empty(result); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_not_create_enriched_events_when_mentions_is_null() |
|||
{ |
|||
var envelope = Envelope.Create<AppEvent>(new CommentCreated { Mentions = null }); |
|||
|
|||
var result = await sut.CreateEnrichedEventsAsync(envelope); |
|||
|
|||
Assert.Empty(result); |
|||
|
|||
A.CallTo(() => userResolver.QueryManyAsync(A<string[]>.Ignored)) |
|||
.MustNotHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_not_create_enriched_events_when_mentions_is_empty() |
|||
{ |
|||
var envelope = Envelope.Create<AppEvent>(new CommentCreated { Mentions = new string[0] }); |
|||
|
|||
var result = await sut.CreateEnrichedEventsAsync(envelope); |
|||
|
|||
Assert.Empty(result); |
|||
|
|||
A.CallTo(() => userResolver.QueryManyAsync(A<string[]>.Ignored)) |
|||
.MustNotHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_skip_udated_event() |
|||
{ |
|||
var envelope = Envelope.Create<AppEvent>(new CommentUpdated()); |
|||
|
|||
var result = await sut.CreateEnrichedEventsAsync(envelope); |
|||
|
|||
Assert.Empty(result); |
|||
|
|||
A.CallTo(() => userResolver.QueryManyAsync(A<string[]>.Ignored)) |
|||
.MustNotHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_skip_deleted_event() |
|||
{ |
|||
var envelope = Envelope.Create<AppEvent>(new CommentDeleted()); |
|||
|
|||
var result = await sut.CreateEnrichedEventsAsync(envelope); |
|||
|
|||
Assert.Empty(result); |
|||
|
|||
A.CallTo(() => userResolver.QueryManyAsync(A<string[]>.Ignored)) |
|||
.MustNotHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_not_trigger_precheck_when_event_type_not_correct() |
|||
{ |
|||
TestForCondition(string.Empty, trigger => |
|||
{ |
|||
var result = sut.Trigger(new ContentCreated(), trigger, Guid.NewGuid()); |
|||
|
|||
Assert.False(result); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_trigger_precheck_when_event_type_correct() |
|||
{ |
|||
TestForCondition(string.Empty, trigger => |
|||
{ |
|||
var result = sut.Trigger(new CommentCreated(), trigger, Guid.NewGuid()); |
|||
|
|||
Assert.True(result); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_not_trigger_check_when_event_type_not_correct() |
|||
{ |
|||
TestForCondition(string.Empty, trigger => |
|||
{ |
|||
var result = sut.Trigger(new EnrichedContentEvent(), trigger); |
|||
|
|||
Assert.False(result); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_trigger_check_when_condition_is_empty() |
|||
{ |
|||
TestForCondition(string.Empty, trigger => |
|||
{ |
|||
var result = sut.Trigger(new EnrichedCommentEvent(), trigger); |
|||
|
|||
Assert.True(result); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_trigger_check_when_condition_matchs() |
|||
{ |
|||
TestForCondition("true", trigger => |
|||
{ |
|||
var result = sut.Trigger(new EnrichedCommentEvent(), trigger); |
|||
|
|||
Assert.True(result); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_not_trigger_check_when_condition_does_not_matchs() |
|||
{ |
|||
TestForCondition("false", trigger => |
|||
{ |
|||
var result = sut.Trigger(new EnrichedCommentEvent(), trigger); |
|||
|
|||
Assert.False(result); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_trigger_check_when_email_is_correct() |
|||
{ |
|||
TestForRealCondition("event.mentionedUser.email == 'sebastian@squidex.io'", (handler, trigger) => |
|||
{ |
|||
var user = CreateUser("1"); |
|||
|
|||
var result = handler.Trigger(new EnrichedCommentEvent { MentionedUser = user }, trigger); |
|||
|
|||
Assert.True(result); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_not_trigger_check_when_email_is_correct() |
|||
{ |
|||
TestForRealCondition("event.mentionedUser.email == 'other@squidex.io'", (handler, trigger) => |
|||
{ |
|||
var user = CreateUser("1"); |
|||
|
|||
var result = handler.Trigger(new EnrichedCommentEvent { MentionedUser = user }, trigger); |
|||
|
|||
Assert.False(result); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_trigger_check_when_text_is_urgent() |
|||
{ |
|||
TestForRealCondition("event.text.indexOf('urgent') >= 0", (handler, trigger) => |
|||
{ |
|||
var text = "Hey man, this is really urgent."; |
|||
|
|||
var result = handler.Trigger(new EnrichedCommentEvent { Text = text }, trigger); |
|||
|
|||
Assert.True(result); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_not_trigger_check_when_text_is_not_urgent() |
|||
{ |
|||
TestForRealCondition("event.text.indexOf('urgent') >= 0", (handler, trigger) => |
|||
{ |
|||
var text = "Hey man, just an information for you."; |
|||
|
|||
var result = handler.Trigger(new EnrichedCommentEvent { Text = text }, trigger); |
|||
|
|||
Assert.False(result); |
|||
}); |
|||
} |
|||
|
|||
private IUser CreateUser(string id, string email = "sebastian@squidex.io") |
|||
{ |
|||
var user = A.Fake<IUser>(); |
|||
|
|||
A.CallTo(() => user.Id).Returns(id); |
|||
A.CallTo(() => user.Email).Returns(email); |
|||
|
|||
return user; |
|||
} |
|||
|
|||
private void TestForRealCondition(string condition, Action<IRuleTriggerHandler, CommentTrigger> action) |
|||
{ |
|||
var trigger = new CommentTrigger { Condition = condition }; |
|||
|
|||
var handler = new CommentTriggerHandler(new JintScriptEngine(), userResolver); |
|||
|
|||
action(handler, trigger); |
|||
} |
|||
|
|||
private void TestForCondition(string condition, Action<CommentTrigger> action) |
|||
{ |
|||
var trigger = new CommentTrigger { Condition = condition }; |
|||
|
|||
action(trigger); |
|||
|
|||
if (string.IsNullOrWhiteSpace(condition)) |
|||
{ |
|||
A.CallTo(() => scriptEngine.Evaluate("event", A<object>.Ignored, condition)) |
|||
.MustNotHaveHappened(); |
|||
} |
|||
else |
|||
{ |
|||
A.CallTo(() => scriptEngine.Evaluate("event", A<object>.Ignored, condition)) |
|||
.MustHaveHappened(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
<div [formGroup]="triggerForm" class="form-horizontal"> |
|||
<div class="form-group"> |
|||
<label for="condition">Condition</label> |
|||
|
|||
<sqx-control-errors for="condition" [submitted]="triggerFormSubmitted"></sqx-control-errors> |
|||
|
|||
<textarea class="form-control code" id="condition" formControlName="condition" placeholder="Optional condition as javascript expression"></textarea> |
|||
</div> |
|||
|
|||
<div class="help"> |
|||
<h4>Conditions</h4> |
|||
|
|||
<p>Conditions are javascript expressions that define when to trigger, for example:</p> |
|||
|
|||
<ul class="help-examples"> |
|||
<li class="help-example"> |
|||
Specific users:<br/> |
|||
|
|||
<sqx-code>event.mentionedUser.email === 'mail2stehle@gmail.com'</sqx-code> |
|||
</li> |
|||
<li class="help-example"> |
|||
Only for text keywords:<br/> |
|||
|
|||
<sqx-code>event.text.indexOf('urgent') >= 0</sqx-code> |
|||
</li> |
|||
</ul> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,6 @@ |
|||
@import '_vars'; |
|||
@import '_mixins'; |
|||
|
|||
textarea { |
|||
height: 100px; |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { Component, Input, OnInit } from '@angular/core'; |
|||
import { FormControl, FormGroup } from '@angular/forms'; |
|||
|
|||
@Component({ |
|||
selector: 'sqx-comment-trigger', |
|||
styleUrls: ['./comment-trigger.component.scss'], |
|||
templateUrl: './comment-trigger.component.html' |
|||
}) |
|||
export class CommentTriggerComponent implements OnInit { |
|||
@Input() |
|||
public trigger: any; |
|||
|
|||
@Input() |
|||
public triggerForm: FormGroup; |
|||
|
|||
@Input() |
|||
public triggerFormSubmitted = false; |
|||
|
|||
public ngOnInit() { |
|||
this.triggerForm.setControl('condition', |
|||
new FormControl(this.trigger.condition || '')); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue