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