Browse Source

Schema triggers (#912)

* Fix to schema triggers

* Improve tests.
pull/915/head
Sebastian Stehle 3 years ago
committed by GitHub
parent
commit
6e7828b90c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs
  2. 33
      backend/src/Squidex.Domain.Apps.Entities/Comments/CommentTriggerHandler.cs
  3. 3
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs
  4. 3
      backend/src/Squidex.Domain.Apps.Entities/Rules/ManualTriggerHandler.cs
  5. 2
      backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs
  6. 3
      backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaChangedTriggerHandler.cs
  7. 25
      backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestUtils.cs
  8. 24
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs
  9. 12
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentTriggerHandlerTests.cs
  10. 22
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs
  11. 16
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/ManualTriggerHandlerTests.cs
  12. 21
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs
  13. 3
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/UsageTracking/UsageTriggerHandlerTests.cs
  14. 23
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaChangedTriggerHandlerTests.cs
  15. 103
      backend/tools/TestSuite/TestSuite.ApiTests/RuleRunnerTests.cs
  16. 6
      frontend/src/app/features/content/shared/forms/array-editor.component.html
  17. 18
      frontend/src/app/features/content/shared/forms/array-editor.component.scss
  18. 3
      frontend/src/app/features/content/shared/forms/array-editor.component.ts
  19. 6
      frontend/src/app/features/content/shared/forms/assets-editor.component.html
  20. 30
      frontend/src/app/features/content/shared/forms/assets-editor.component.scss
  21. 3
      frontend/src/app/features/content/shared/forms/assets-editor.component.ts
  22. 7
      frontend/src/app/features/content/shared/forms/field-editor.component.html
  23. 1
      frontend/src/app/features/content/shared/forms/field-editor.component.scss
  24. 2
      frontend/src/app/features/content/shared/references/references-editor.component.html
  25. 23
      frontend/src/app/features/content/shared/references/references-editor.component.scss
  26. 3
      frontend/src/app/features/content/shared/references/references-editor.component.ts
  27. 2
      frontend/src/app/theme/_mixins.scss
  28. 1
      frontend/src/app/theme/_vars.scss

3
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs

@ -86,6 +86,9 @@ namespace Squidex.Domain.Apps.Entities.Assets
result.AssetType = asset.Type; result.AssetType = asset.Type;
} }
// Use the concrete event to map properties that are not part of app event.
SimpleMapper.Map(assetEvent, result);
switch (@event.Payload) switch (@event.Payload)
{ {
case AssetCreated: case AssetCreated:

33
backend/src/Squidex.Domain.Apps.Entities/Comments/CommentTriggerHandler.cs

@ -42,26 +42,31 @@ namespace Squidex.Domain.Apps.Entities.Comments
{ {
var commentCreated = (CommentCreated)@event.Payload; var commentCreated = (CommentCreated)@event.Payload;
if (commentCreated.Mentions?.Length > 0) if (!(commentCreated.Mentions?.Length > 0))
{ {
var users = await userResolver.QueryManyAsync(commentCreated.Mentions, ct); yield break;
}
var users = await userResolver.QueryManyAsync(commentCreated.Mentions, ct);
if (users.Count <= 0)
{
yield break;
}
if (users.Count > 0) foreach (var user in users.Values)
{
var enrichedEvent = new EnrichedCommentEvent
{ {
foreach (var user in users.Values) MentionedUser = user
{ };
var enrichedEvent = new EnrichedCommentEvent
{
MentionedUser = user
};
enrichedEvent.Name = "UserMentioned"; enrichedEvent.Name = "UserMentioned";
SimpleMapper.Map(commentCreated, enrichedEvent); // Use the concrete event to map properties that are not part of app event.
SimpleMapper.Map(commentCreated, enrichedEvent);
yield return enrichedEvent; yield return enrichedEvent;
}
}
} }
} }

3
backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs

@ -93,6 +93,9 @@ namespace Squidex.Domain.Apps.Entities.Contents
SimpleMapper.Map(content, result); SimpleMapper.Map(content, result);
} }
// Use the concrete event to map properties that are not part of app event.
SimpleMapper.Map(contentEvent, result);
switch (@event.Payload) switch (@event.Payload)
{ {
case ContentCreated: case ContentCreated:

3
backend/src/Squidex.Domain.Apps.Entities/Rules/ManualTriggerHandler.cs

@ -30,7 +30,8 @@ namespace Squidex.Domain.Apps.Entities.Rules
{ {
var result = new EnrichedManualEvent(); var result = new EnrichedManualEvent();
SimpleMapper.Map(@event.Payload, result); // Use the concrete event to map properties that are not part of app event.
SimpleMapper.Map((RuleManuallyTriggered)@event.Payload, result);
await Task.Yield(); await Task.Yield();

2
backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs

@ -61,7 +61,7 @@ namespace Squidex.Domain.Apps.Entities.Rules
await foreach (var job in jobs) await foreach (var job in jobs)
{ {
// We do not want to handle disabled rules in the normal flow. // We do not want to handle disabled rules in the normal flow.
if (job.Job != null && job.SkipReason == SkipReason.None) if (job.Job != null && job.SkipReason is SkipReason.None or SkipReason.Failed)
{ {
await ruleEventRepository.EnqueueAsync(job.Job, job.EnrichmentError); await ruleEventRepository.EnqueueAsync(job.Job, job.EnrichmentError);
} }

3
backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaChangedTriggerHandler.cs

@ -38,7 +38,8 @@ namespace Squidex.Domain.Apps.Entities.Schemas
{ {
var result = new EnrichedSchemaEvent(); var result = new EnrichedSchemaEvent();
SimpleMapper.Map(@event.Payload, result); // Use the concrete event to map properties that are not part of app event.
SimpleMapper.Map((SchemaEvent)@event.Payload, result);
switch (@event.Payload) switch (@event.Payload)
{ {

25
backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestUtils.cs

@ -24,6 +24,7 @@ using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.Json; using Squidex.Domain.Apps.Core.Rules.Json;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.Schemas.Json; using Squidex.Domain.Apps.Core.Schemas.Json;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json;
@ -183,5 +184,29 @@ namespace Squidex.Domain.Apps.Core.TestHelpers
return DefaultSerializer.Serialize(document, true); return DefaultSerializer.Serialize(document, true);
} }
public static TEvent CreateEvent<TEvent>(Action<TEvent>? init = null) where TEvent : IEvent, new()
{
var result = new TEvent();
if (result is SquidexEvent squidexEvent)
{
squidexEvent.Actor = new RefToken(RefTokenType.Client, "my-client");
}
if (result is AppEvent appEvent)
{
appEvent.AppId = NamedId.Of(DomainId.NewGuid(), "my-app");
}
if (result is SchemaEvent schemaEvent)
{
schemaEvent.SchemaId = NamedId.Of(DomainId.NewGuid(), "my-schema");
}
init?.Invoke(result);
return result;
}
} }
} }

24
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs

@ -11,6 +11,7 @@ using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Assets; using Squidex.Domain.Apps.Events.Assets;
@ -45,10 +46,10 @@ namespace Squidex.Domain.Apps.Entities.Assets
public static IEnumerable<object[]> TestEvents() public static IEnumerable<object[]> TestEvents()
{ {
yield return new object[] { new AssetCreated(), EnrichedAssetEventType.Created }; yield return new object[] { TestUtils.CreateEvent<AssetCreated>(), EnrichedAssetEventType.Created };
yield return new object[] { new AssetUpdated(), EnrichedAssetEventType.Updated }; yield return new object[] { TestUtils.CreateEvent<AssetUpdated>(), EnrichedAssetEventType.Updated };
yield return new object[] { new AssetAnnotated(), EnrichedAssetEventType.Annotated }; yield return new object[] { TestUtils.CreateEvent<AssetAnnotated>(), EnrichedAssetEventType.Annotated };
yield return new object[] { new AssetDeleted(), EnrichedAssetEventType.Deleted }; yield return new object[] { TestUtils.CreateEvent<AssetDeleted>(), EnrichedAssetEventType.Deleted };
} }
[Fact] [Fact]
@ -99,9 +100,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
[MemberData(nameof(TestEvents))] [MemberData(nameof(TestEvents))]
public async Task Should_create_enriched_events(AssetEvent @event, EnrichedAssetEventType type) public async Task Should_create_enriched_events(AssetEvent @event, EnrichedAssetEventType type)
{ {
var ctx = Context(); var ctx = Context(appId: @event.AppId);
@event.AppId = ctx.AppId;
var envelope = Envelope.Create<AppEvent>(@event).SetEventStreamNumber(12); var envelope = Envelope.Create<AppEvent>(@event).SetEventStreamNumber(12);
@ -110,9 +109,12 @@ namespace Squidex.Domain.Apps.Entities.Assets
var result = await sut.CreateEnrichedEventsAsync(envelope, ctx, ct).ToListAsync(ct); var result = await sut.CreateEnrichedEventsAsync(envelope, ctx, ct).ToListAsync(ct);
var enrichedEvent = result.Single() as EnrichedAssetEvent; var enrichedEvent = (EnrichedAssetEvent)result.Single();
Assert.Equal(type, enrichedEvent!.Type); Assert.Equal(type, enrichedEvent.Type);
Assert.Equal(@event.Actor, enrichedEvent.Actor);
Assert.Equal(@event.AppId, enrichedEvent.AppId);
Assert.Equal(@event.AppId.Id, enrichedEvent.AppId.Id);
} }
[Fact] [Fact]
@ -175,13 +177,13 @@ namespace Squidex.Domain.Apps.Entities.Assets
} }
} }
private static RuleContext Context(RuleTrigger? trigger = null) private static RuleContext Context(RuleTrigger? trigger = null, NamedId<DomainId>? appId = null)
{ {
trigger ??= new AssetChangedTriggerV2(); trigger ??= new AssetChangedTriggerV2();
return new RuleContext return new RuleContext
{ {
AppId = NamedId.Of(DomainId.NewGuid(), "my-app"), AppId = appId ?? NamedId.Of(DomainId.NewGuid(), "my-app"),
Rule = new Rule(trigger, A.Fake<RuleAction>()), Rule = new Rule(trigger, A.Fake<RuleAction>()),
RuleId = DomainId.NewGuid() RuleId = DomainId.NewGuid()
}; };

12
backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentTriggerHandlerTests.cs

@ -77,11 +77,12 @@ namespace Squidex.Domain.Apps.Entities.Comments
var userIds = users.Select(x => x.Id).ToArray(); var userIds = users.Select(x => x.Id).ToArray();
var @event = new CommentCreated { Mentions = userIds }; var @event = new CommentCreated { Mentions = userIds };
var envelope = Envelope.Create<AppEvent>(@event);
A.CallTo(() => userResolver.QueryManyAsync(userIds, default)) A.CallTo(() => userResolver.QueryManyAsync(userIds, default))
.Returns(users.ToDictionary(x => x.Id)); .Returns(users.ToDictionary(x => x.Id));
var result = await sut.CreateEnrichedEventsAsync(Envelope.Create<AppEvent>(@event), ctx, default).ToListAsync(); var result = await sut.CreateEnrichedEventsAsync(envelope, ctx, default).ToListAsync();
Assert.Equal(2, result.Count); Assert.Equal(2, result.Count);
@ -106,8 +107,9 @@ namespace Squidex.Domain.Apps.Entities.Comments
var userIds = users.Select(x => x.Id).ToArray(); var userIds = users.Select(x => x.Id).ToArray();
var @event = new CommentCreated { Mentions = userIds }; var @event = new CommentCreated { Mentions = userIds };
var envelope = Envelope.Create<AppEvent>(@event);
var result = await sut.CreateEnrichedEventsAsync(Envelope.Create<AppEvent>(@event), ctx, default).ToListAsync(); var result = await sut.CreateEnrichedEventsAsync(envelope, ctx, default).ToListAsync();
Assert.Empty(result); Assert.Empty(result);
} }
@ -118,8 +120,9 @@ namespace Squidex.Domain.Apps.Entities.Comments
var ctx = Context(); var ctx = Context();
var @event = new CommentCreated { Mentions = null }; var @event = new CommentCreated { Mentions = null };
var envelope = Envelope.Create<AppEvent>(@event);
var result = await sut.CreateEnrichedEventsAsync(Envelope.Create<AppEvent>(@event), ctx, default).ToListAsync(); var result = await sut.CreateEnrichedEventsAsync(envelope, ctx, default).ToListAsync();
Assert.Empty(result); Assert.Empty(result);
@ -133,8 +136,9 @@ namespace Squidex.Domain.Apps.Entities.Comments
var ctx = Context(); var ctx = Context();
var @event = new CommentCreated { Mentions = Array.Empty<string>() }; var @event = new CommentCreated { Mentions = Array.Empty<string>() };
var envelope = Envelope.Create<AppEvent>(@event);
var result = await sut.CreateEnrichedEventsAsync(Envelope.Create<AppEvent>(@event), ctx, default).ToListAsync(); var result = await sut.CreateEnrichedEventsAsync(envelope, ctx, default).ToListAsync();
Assert.Empty(result); Assert.Empty(result);

22
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs

@ -20,6 +20,7 @@ using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Collections;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection;
using Xunit; using Xunit;
namespace Squidex.Domain.Apps.Entities.Contents namespace Squidex.Domain.Apps.Entities.Contents
@ -50,12 +51,12 @@ namespace Squidex.Domain.Apps.Entities.Contents
public static IEnumerable<object[]> TestEvents() public static IEnumerable<object[]> TestEvents()
{ {
yield return new object[] { new ContentCreated(), EnrichedContentEventType.Created }; yield return new object[] { TestUtils.CreateEvent<ContentCreated>(), EnrichedContentEventType.Created };
yield return new object[] { new ContentUpdated(), EnrichedContentEventType.Updated }; yield return new object[] { TestUtils.CreateEvent<ContentUpdated>(), EnrichedContentEventType.Updated };
yield return new object[] { new ContentDeleted(), EnrichedContentEventType.Deleted }; yield return new object[] { TestUtils.CreateEvent<ContentDeleted>(), EnrichedContentEventType.Deleted };
yield return new object[] { new ContentStatusChanged { Change = StatusChange.Change }, EnrichedContentEventType.StatusChanged }; yield return new object[] { TestUtils.CreateEvent<ContentStatusChanged>(x => x.Change = StatusChange.Change), EnrichedContentEventType.StatusChanged };
yield return new object[] { new ContentStatusChanged { Change = StatusChange.Published }, EnrichedContentEventType.Published }; yield return new object[] { TestUtils.CreateEvent<ContentStatusChanged>(x => x.Change = StatusChange.Published), EnrichedContentEventType.Published };
yield return new object[] { new ContentStatusChanged { Change = StatusChange.Unpublished }, EnrichedContentEventType.Unpublished }; yield return new object[] { TestUtils.CreateEvent<ContentStatusChanged>(x => x.Change = StatusChange.Unpublished), EnrichedContentEventType.Unpublished };
} }
[Fact] [Fact]
@ -188,13 +189,18 @@ namespace Squidex.Domain.Apps.Entities.Contents
var envelope = Envelope.Create<AppEvent>(@event).SetEventStreamNumber(12); var envelope = Envelope.Create<AppEvent>(@event).SetEventStreamNumber(12);
A.CallTo(() => contentLoader.GetAsync(ctx.AppId.Id, @event.ContentId, 12, ct)) A.CallTo(() => contentLoader.GetAsync(ctx.AppId.Id, @event.ContentId, 12, ct))
.Returns(new ContentEntity { AppId = ctx.AppId, SchemaId = schemaMatch }); .Returns(SimpleMapper.Map(@event, new ContentEntity()));
var result = await sut.CreateEnrichedEventsAsync(envelope, ctx, ct).ToListAsync(ct); var result = await sut.CreateEnrichedEventsAsync(envelope, ctx, ct).ToListAsync(ct);
var enrichedEvent = result.Single() as EnrichedContentEvent; var enrichedEvent = (EnrichedContentEvent)result.Single();
Assert.Equal(type, enrichedEvent!.Type); Assert.Equal(type, enrichedEvent!.Type);
Assert.Equal(@event.Actor, enrichedEvent.Actor);
Assert.Equal(@event.AppId, enrichedEvent.AppId);
Assert.Equal(@event.AppId.Id, enrichedEvent.AppId.Id);
Assert.Equal(@event.SchemaId, enrichedEvent.SchemaId);
Assert.Equal(@event.SchemaId.Id, enrichedEvent.SchemaId.Id);
} }
[Fact] [Fact]

16
backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/ManualTriggerHandlerTests.cs

@ -5,8 +5,10 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using FakeItEasy;
using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Rules; using Squidex.Domain.Apps.Events.Rules;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -36,11 +38,16 @@ namespace Squidex.Domain.Apps.Entities.Rules
[Fact] [Fact]
public async Task Should_create_event_with_name() public async Task Should_create_event_with_name()
{ {
var @event = new RuleManuallyTriggered(); var @event = TestUtils.CreateEvent<RuleManuallyTriggered>();
var envelope = Envelope.Create<AppEvent>(@event);
var result = await sut.CreateEnrichedEventsAsync(envelope, default, default).ToListAsync();
var result = await sut.CreateEnrichedEventsAsync(Envelope.Create<AppEvent>(@event), default, default).ToListAsync(); var enrichedEvent = (EnrichedManualEvent)result.Single();
Assert.NotEmpty(result); Assert.Equal(@event.Actor, enrichedEvent.Actor);
Assert.Equal(@event.AppId, enrichedEvent.AppId);
Assert.Equal(@event.AppId.Id, enrichedEvent.AppId.Id);
} }
[Fact] [Fact]
@ -49,8 +56,9 @@ namespace Squidex.Domain.Apps.Entities.Rules
var actor = RefToken.User("me"); var actor = RefToken.User("me");
var @event = new RuleManuallyTriggered { Actor = actor }; var @event = new RuleManuallyTriggered { Actor = actor };
var envelope = Envelope.Create<AppEvent>(@event);
var result = await sut.CreateEnrichedEventsAsync(Envelope.Create<AppEvent>(@event), default, default).ToListAsync(); var result = await sut.CreateEnrichedEventsAsync(envelope, default, default).ToListAsync();
Assert.Equal(actor, ((EnrichedUserEventBase)result.Single()).Actor); Assert.Equal(actor, ((EnrichedUserEventBase)result.Single()).Actor);
} }

21
backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs

@ -135,6 +135,27 @@ namespace Squidex.Domain.Apps.Entities.Rules
.MustHaveHappened(); .MustHaveHappened();
} }
[Fact]
public async Task Should_update_repository_if_enqueing_broken_job()
{
var @event = Envelope.Create<IEvent>(new ContentCreated { AppId = appId });
var rule = CreateRule();
var job = new RuleJob
{
Created = now
};
A.CallTo(() => ruleService.CreateJobsAsync(@event, MatchingContext(rule), default))
.Returns(new List<JobResult> { new JobResult { Job = job, SkipReason = SkipReason.Failed } }.ToAsyncEnumerable());
await sut.EnqueueAsync(rule.RuleDef, rule.Id, @event);
A.CallTo(() => ruleEventRepository.EnqueueAsync(job, (Exception?)null, default))
.MustHaveHappened();
}
[Fact] [Fact]
public async Task Should_update_repository_with_jobs_from_service() public async Task Should_update_repository_with_jobs_from_service()
{ {

3
backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/UsageTracking/UsageTriggerHandlerTests.cs

@ -46,8 +46,9 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking
var ctx = Context(); var ctx = Context();
var @event = new AppUsageExceeded { CallsCurrent = 80, CallsLimit = 120 }; var @event = new AppUsageExceeded { CallsCurrent = 80, CallsLimit = 120 };
var envelope = Envelope.Create<AppEvent>(@event);
var result = await sut.CreateEnrichedEventsAsync(Envelope.Create<AppEvent>(@event), ctx, default).ToListAsync(); var result = await sut.CreateEnrichedEventsAsync(envelope, ctx, default).ToListAsync();
var enrichedEvent = result.Single() as EnrichedUsageExceededEvent; var enrichedEvent = result.Single() as EnrichedUsageExceededEvent;

23
backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaChangedTriggerHandlerTests.cs

@ -11,6 +11,8 @@ using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Apps; using Squidex.Domain.Apps.Events.Apps;
using Squidex.Domain.Apps.Events.Schemas; using Squidex.Domain.Apps.Events.Schemas;
@ -38,11 +40,11 @@ namespace Squidex.Domain.Apps.Entities.Schemas
public static IEnumerable<object[]> TestEvents() public static IEnumerable<object[]> TestEvents()
{ {
yield return new object[] { new SchemaCreated(), EnrichedSchemaEventType.Created }; yield return new object[] { TestUtils.CreateEvent<SchemaCreated>(), EnrichedSchemaEventType.Created };
yield return new object[] { new SchemaUpdated(), EnrichedSchemaEventType.Updated }; yield return new object[] { TestUtils.CreateEvent<SchemaUpdated>(), EnrichedSchemaEventType.Updated };
yield return new object[] { new SchemaDeleted(), EnrichedSchemaEventType.Deleted }; yield return new object[] { TestUtils.CreateEvent<SchemaDeleted>(), EnrichedSchemaEventType.Deleted };
yield return new object[] { new SchemaPublished(), EnrichedSchemaEventType.Published }; yield return new object[] { TestUtils.CreateEvent<SchemaPublished>(), EnrichedSchemaEventType.Published };
yield return new object[] { new SchemaUnpublished(), EnrichedSchemaEventType.Unpublished }; yield return new object[] { TestUtils.CreateEvent<SchemaUnpublished>(), EnrichedSchemaEventType.Unpublished };
} }
[Fact] [Fact]
@ -67,7 +69,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
[MemberData(nameof(TestEvents))] [MemberData(nameof(TestEvents))]
public async Task Should_create_enriched_events(SchemaEvent @event, EnrichedSchemaEventType type) public async Task Should_create_enriched_events(SchemaEvent @event, EnrichedSchemaEventType type)
{ {
var ctx = Context(); var ctx = Context(appId: @event.AppId);
var envelope = Envelope.Create<AppEvent>(@event).SetEventStreamNumber(12); var envelope = Envelope.Create<AppEvent>(@event).SetEventStreamNumber(12);
@ -76,6 +78,11 @@ namespace Squidex.Domain.Apps.Entities.Schemas
var enrichedEvent = result.Single() as EnrichedSchemaEvent; var enrichedEvent = result.Single() as EnrichedSchemaEvent;
Assert.Equal(type, enrichedEvent!.Type); Assert.Equal(type, enrichedEvent!.Type);
Assert.Equal(@event.Actor, enrichedEvent.Actor);
Assert.Equal(@event.AppId, enrichedEvent.AppId);
Assert.Equal(@event.AppId.Id, enrichedEvent.AppId.Id);
Assert.Equal(@event.SchemaId, enrichedEvent.SchemaId);
Assert.Equal(@event.SchemaId.Id, enrichedEvent.SchemaId.Id);
} }
[Fact] [Fact]
@ -151,13 +158,13 @@ namespace Squidex.Domain.Apps.Entities.Schemas
} }
} }
private static RuleContext Context(RuleTrigger? trigger = null) private static RuleContext Context(RuleTrigger? trigger = null, NamedId<DomainId>? appId = null)
{ {
trigger ??= new SchemaChangedTrigger(); trigger ??= new SchemaChangedTrigger();
return new RuleContext return new RuleContext
{ {
AppId = NamedId.Of(DomainId.NewGuid(), "my-app"), AppId = appId ?? NamedId.Of(DomainId.NewGuid(), "my-app"),
Rule = new Rule(trigger, A.Fake<RuleAction>()), Rule = new Rule(trigger, A.Fake<RuleAction>()),
RuleId = DomainId.NewGuid() RuleId = DomainId.NewGuid()
}; };

103
backend/tools/TestSuite/TestSuite.ApiTests/RuleRunnerTests.cs

@ -31,7 +31,7 @@ namespace TestSuite.ApiTests
} }
[Fact] [Fact]
public async Task Should_run_rules() public async Task Should_run_rules_on_content_change()
{ {
// STEP 0: Create app. // STEP 0: Create app.
await CreateAppAsync(); await CreateAppAsync();
@ -77,6 +77,94 @@ namespace TestSuite.ApiTests
Assert.Single(eventsRule.Items); Assert.Single(eventsRule.Items);
} }
[Fact]
public async Task Should_run_rules_on_asset_change()
{
// STEP 0: Create app.
await CreateAppAsync();
// STEP 1: Start webhook session
var (url, sessionId) = await webhookCatcher.CreateSessionAsync();
// STEP 2: Create rule
var createRule = new CreateRuleDto
{
Action = new WebhookRuleActionDto
{
Method = WebhookMethod.POST,
Payload = null,
PayloadType = null,
Url = new Uri(url)
},
Trigger = new AssetChangedRuleTriggerDto()
};
var rule = await _.Rules.PostRuleAsync(appName, createRule);
// STEP 3: Create test asset
await CreateAssetAsync();
// Get requests.
var requests = await webhookCatcher.WaitForRequestsAsync(sessionId, TimeSpan.FromMinutes(2));
Assert.Contains(requests, x => x.Method == "POST" && x.Content.Contains("logo-squared", StringComparison.OrdinalIgnoreCase));
// STEP 4: Get events
var eventsAll = await _.Rules.GetEventsAsync(appName, rule.Id);
var eventsRule = await _.Rules.GetEventsAsync(appName);
Assert.Single(eventsAll.Items);
Assert.Single(eventsRule.Items);
}
[Fact]
public async Task Should_run_rules_on_schema_change()
{
// STEP 0: Create app.
await CreateAppAsync();
// STEP 1: Start webhook session
var (url, sessionId) = await webhookCatcher.CreateSessionAsync();
// STEP 2: Create rule
var createRule = new CreateRuleDto
{
Action = new WebhookRuleActionDto
{
Method = WebhookMethod.POST,
Payload = null,
PayloadType = null,
Url = new Uri(url)
},
Trigger = new SchemaChangedRuleTriggerDto()
};
var rule = await _.Rules.PostRuleAsync(appName, createRule);
// STEP 3: Create test schema
await TestEntity.CreateSchemaAsync(_.Schemas, appName, schemaName);
// Get requests.
var requests = await webhookCatcher.WaitForRequestsAsync(sessionId, TimeSpan.FromMinutes(2));
Assert.Contains(requests, x => x.Method == "POST" && x.Content.Contains(schemaName, StringComparison.OrdinalIgnoreCase));
// STEP 4: Get events
var eventsAll = await _.Rules.GetEventsAsync(appName, rule.Id);
var eventsRule = await _.Rules.GetEventsAsync(appName);
Assert.Single(eventsAll.Items);
Assert.Single(eventsRule.Items);
}
[Fact] [Fact]
public async Task Should_run_rule_manually() public async Task Should_run_rule_manually()
{ {
@ -182,6 +270,19 @@ namespace TestSuite.ApiTests
}); });
} }
private async Task CreateAssetAsync()
{
// Upload a test asset
var fileInfo = new FileInfo("Assets/logo-squared.png");
await using (var stream = fileInfo.OpenRead())
{
var upload = new FileParameter(stream, fileInfo.Name, "image/png");
await _.Assets.PostAssetAsync(appName, file: upload);
}
}
private async Task CreateAppAsync() private async Task CreateAppAsync()
{ {
var createRequest = new CreateAppDto var createRequest = new CreateAppDto

6
frontend/src/app/features/content/shared/forms/array-editor.component.html

@ -1,5 +1,5 @@
<ng-container *ngIf="formModel.itemChanges | async; let items"> <ng-container *ngIf="formModel.itemChanges | async; let items">
<div class="array-container" *ngIf="items.length > 0 && items.length <= 20;" <div class="array-container" [class.expanded]="isExpanded" *ngIf="items.length > 0 && items.length <= 20;"
cdkDropList cdkDropList
[cdkDropListDisabled]="false" [cdkDropListDisabled]="false"
[cdkDropListData]="items" [cdkDropListData]="items"
@ -31,7 +31,7 @@
</div> </div>
</div> </div>
<div class="array-container" *ngIf="items.length > 20"> <div class="array-container" [class.expanded]="isExpanded" *ngIf="items.length > 20">
<virtual-scroller #scroll [items]="$any(items)" [enableUnequalChildrenSizes]="true"> <virtual-scroller #scroll [items]="$any(items)" [enableUnequalChildrenSizes]="true">
<div *ngFor="let itemForm of scroll.viewPortItems; index as i" class="item" <div *ngFor="let itemForm of scroll.viewPortItems; index as i" class="item"
[class.first]="scroll.viewPortInfo.startIndexWithBuffer + i === 0" [class.first]="scroll.viewPortInfo.startIndexWithBuffer + i === 0"
@ -59,7 +59,7 @@
</virtual-scroller> </virtual-scroller>
</div> </div>
<div class="row g-0 align-items-center"> <div class="array-buttons row g-0 align-items-center" [class.expanded]="isExpanded">
<div class="col-auto"> <div class="col-auto">
<ng-container *ngIf="isArray; else component"> <ng-container *ngIf="isArray; else component">
<ng-container *ngIf="hasField"> <ng-container *ngIf="hasField">

18
frontend/src/app/features/content/shared/forms/array-editor.component.scss

@ -23,6 +23,24 @@ virtual-scroller {
background: $color-border-lighter; background: $color-border-lighter;
margin: 0; margin: 0;
margin-bottom: 1rem; margin-bottom: 1rem;
overflow-y: auto;
&.expanded {
@include absolute(3rem, 1.25rem, 2.375rem + 1rem + .75rem, 1.25rem);
margin: 0;
virtual-scroller {
@include absolute(0, 0, 0, 0);
height: 100%;
}
}
}
.array-buttons {
&.expanded {
@include absolute(null, 1.25rem, .75rem, 1.25rem);
max-height: none;
}
} }
.drag-container { .drag-container {

3
frontend/src/app/features/content/shared/forms/array-editor.component.ts

@ -35,6 +35,9 @@ export class ArrayEditorComponent implements OnChanges {
@Input() @Input()
public isComparing = false; public isComparing = false;
@Input()
public isExpanded = false;
@Input() @Input()
public canUnset?: boolean | null; public canUnset?: boolean | null;

6
frontend/src/app/features/content/shared/forms/assets-editor.component.html

@ -1,4 +1,8 @@
<div class="assets-container" (sqxDropFile)="addFiles($event)" [sqxDropDisabled]="snapshot.isDisabled" tabindex="1000"> <div class="assets-container"
[class.expanded]="isExpanded"
(sqxDropFile)="addFiles($event)"
[sqxDropDisabled]="snapshot.isDisabled"
tabindex="1000">
<div class="header list"> <div class="header list">
<div class="row gx-2"> <div class="row gx-2">
<div class="col" [class.disabled]="snapshot.isDisabled"> <div class="col" [class.disabled]="snapshot.isDisabled">

30
frontend/src/app/features/content/shared/forms/assets-editor.component.scss

@ -1,19 +1,23 @@
@import 'mixins'; @import 'mixins';
@import 'vars'; @import 'vars';
.disabled { .assets-container {
pointer-events: none; background: $color-background;
max-height: 26.5rem;
min-height: 26.5rem;
overflow-x: hidden;
overflow-y: scroll;
padding: 1rem;
padding-bottom: 0;
&.expanded {
@include absolute(3rem, 1.25rem, .75rem, 1.25rem);
max-height: none;
}
} }
.assets { .disabled {
&-container { pointer-events: none;
background: $color-background;
height: $asset-height + 2rem;
overflow-x: hidden;
overflow-y: scroll;
padding: 1rem;
padding-bottom: 0;
}
} }
.list-view { .list-view {
@ -26,10 +30,6 @@
min-height: 1px; min-height: 1px;
} }
.unrow {
display: block;
}
.drop-area { .drop-area {
@include truncate-nowidth; @include truncate-nowidth;
border: 2px dashed darken($color-border, 10%); border: 2px dashed darken($color-border, 10%);

3
frontend/src/app/features/content/shared/forms/assets-editor.component.ts

@ -50,6 +50,9 @@ export class AssetsEditorComponent extends StatefulControlComponent<State, Reado
@Input() @Input()
public folderId?: string; public folderId?: string;
@Input()
public isExpanded = false;
@Input() @Input()
public set disabled(value: boolean | undefined | null) { public set disabled(value: boolean | undefined | null) {
this.setDisabledState(value === true); this.setDisabledState(value === true);

7
frontend/src/app/features/content/shared/forms/field-editor.component.html

@ -48,12 +48,13 @@
[formModel]="$any(formModel)" [formModel]="$any(formModel)"
[formContext]="formContext" [formContext]="formContext"
[isComparing]="isComparing" [isComparing]="isComparing"
[isExpanded]="isExpanded"
[language]="language" [language]="language"
[languages]="languages"> [languages]="languages">
</sqx-array-editor> </sqx-array-editor>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'Assets'"> <ng-container *ngSwitchCase="'Assets'">
<sqx-assets-editor [formControl]="$any(fieldForm)" [folderId]="field.rawProperties.folderId"></sqx-assets-editor> <sqx-assets-editor [formControl]="$any(fieldForm)" [folderId]="field.rawProperties.folderId" [isExpanded]="isExpanded"></sqx-assets-editor>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'Boolean'"> <ng-container *ngSwitchCase="'Boolean'">
<ng-container [ngSwitch]="field.rawProperties.editor"> <ng-container [ngSwitch]="field.rawProperties.editor">
@ -88,6 +89,7 @@
[formModel]="$any(formModel)" [formModel]="$any(formModel)"
[formContext]="formContext" [formContext]="formContext"
[isComparing]="isComparing" [isComparing]="isComparing"
[isExpanded]="isExpanded"
[language]="language" [language]="language"
[languages]="languages"> [languages]="languages">
</sqx-array-editor> </sqx-array-editor>
@ -124,9 +126,10 @@
<ng-container [ngSwitch]="field.rawProperties.editor"> <ng-container [ngSwitch]="field.rawProperties.editor">
<ng-container *ngSwitchCase="'List'"> <ng-container *ngSwitchCase="'List'">
<sqx-references-editor <sqx-references-editor
[formControl]="$any(fieldForm)"
[allowDuplicates]="field.rawProperties.allowDuplicated" [allowDuplicates]="field.rawProperties.allowDuplicated"
[formContext]="formContext" [formContext]="formContext"
[formControl]="$any(fieldForm)"
[isExpanded]="isExpanded"
[language]="language" [language]="language"
[languages]="languages" [languages]="languages"
[schemaIds]="field.rawProperties.schemaIds"> [schemaIds]="field.rawProperties.schemaIds">

1
frontend/src/app/features/content/shared/forms/field-editor.component.scss

@ -20,6 +20,7 @@
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
padding: .75rem 1.25rem; padding: .75rem 1.25rem;
padding-top: 1.5rem;
z-index: 500; z-index: 500;
} }
} }

2
frontend/src/app/features/content/shared/references/references-editor.component.html

@ -1,4 +1,4 @@
<div class="references-container" <div class="references-container" [class.expanded]="isExpanded"
(sqxResizeCondition)="setCompact($event)" (sqxResizeCondition)="setCompact($event)"
[sqxResizeMinWidth]="600" [sqxResizeMinWidth]="600"
[sqxResizeMaxWidth]="0"> [sqxResizeMaxWidth]="0">

23
frontend/src/app/features/content/shared/references/references-editor.component.scss

@ -1,6 +1,20 @@
@import 'mixins'; @import 'mixins';
@import 'vars'; @import 'vars';
.references-container {
background: $color-background;
max-height: 20rem;
min-height: 6rem;
overflow-x: hidden;
overflow-y: auto;
padding: 1rem;
&.expanded {
@include absolute(3rem, 1.25rem, .75rem, 1.25rem);
max-height: none;
}
}
.disabled { .disabled {
pointer-events: none; pointer-events: none;
} }
@ -13,15 +27,6 @@
text-align: center; text-align: center;
} }
.references-container {
background: $color-background;
max-height: 20rem;
min-height: 6rem;
overflow-x: hidden;
overflow-y: auto;
padding: 1rem;
}
.drop-area { .drop-area {
border: 2px dashed darken($color-border, 10%); border: 2px dashed darken($color-border, 10%);
border-radius: $border-radius; border-radius: $border-radius;

3
frontend/src/app/features/content/shared/references/references-editor.component.ts

@ -44,6 +44,9 @@ export class ReferencesEditorComponent extends StatefulControlComponent<State, R
@Input() @Input()
public formContext!: any; public formContext!: any;
@Input()
public isExpanded = false;
@Input() @Input()
public allowDuplicates?: boolean | null = true; public allowDuplicates?: boolean | null = true;

2
frontend/src/app/theme/_mixins.scss

@ -20,7 +20,7 @@
background: $background; background: $background;
border-color: $color; border-color: $color;
color: $color; color: $color;
z-index: 1000; z-index: 100;
} }
@mixin build-text-button($color) { @mixin build-text-button($color) {

1
frontend/src/app/theme/_vars.scss

@ -85,7 +85,6 @@ $history-dot-sm-offset-x: -($history-dot-sm-size * .5 + 1px);
$asset-width: 14.5rem; $asset-width: 14.5rem;
$asset-folder-height: 4rem; $asset-folder-height: 4rem;
$asset-height: 19rem;
$asset-header: 12rem; $asset-header: 12rem;
$asset-image: 12rem; $asset-image: 12rem;
$asset-footer: 7rem; $asset-footer: 7rem;

Loading…
Cancel
Save