Browse Source

Feature/comment trigger (#469)

* Comment trigger.

* Fixes.

* More backend work.

* User mentioned trigger finished.

* More tests.

* Alternative top query parameter.
pull/470/head
Sebastian Stehle 6 years ago
committed by GitHub
parent
commit
aea1ebc31f
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaAction.cs
  2. 2
      backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs
  3. 2
      backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueActionHandler.cs
  4. 2
      backend/extensions/Squidex.Extensions/Actions/Comment/CommentActionHandler.cs
  5. 2
      backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs
  6. 2
      backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchAction.cs
  7. 2
      backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs
  8. 2
      backend/extensions/Squidex.Extensions/Actions/Email/EmailActionHandler.cs
  9. 2
      backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyActionHandler.cs
  10. 2
      backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaActionHandler.cs
  11. 2
      backend/extensions/Squidex.Extensions/Actions/Medium/MediumActionHandler.cs
  12. 2
      backend/extensions/Squidex.Extensions/Actions/Notification/NotificationActionHandler.cs
  13. 2
      backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderAction.cs
  14. 2
      backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderActionHandler.cs
  15. 2
      backend/extensions/Squidex.Extensions/Actions/Slack/SlackAction.cs
  16. 2
      backend/extensions/Squidex.Extensions/Actions/Slack/SlackActionHandler.cs
  17. 2
      backend/extensions/Squidex.Extensions/Actions/Twitter/TweetAction.cs
  18. 2
      backend/extensions/Squidex.Extensions/Actions/Twitter/TweetActionHandler.cs
  19. 2
      backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookAction.cs
  20. 2
      backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs
  21. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedAssetEvent.cs
  22. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedAssetEventType.cs
  23. 28
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedCommentEvent.cs
  24. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedContentEvent.cs
  25. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedContentEventType.cs
  26. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedEvent.cs
  27. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedManualEvent.cs
  28. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedSchemaEvent.cs
  29. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedSchemaEventBase.cs
  30. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedSchemaEventType.cs
  31. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedUsageExceededEvent.cs
  32. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedUserEventBase.cs
  33. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/IEnrichedEntityEvent.cs
  34. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs
  35. 22
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/CommentTrigger.cs
  36. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventEnricher.cs
  37. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IEventEnricher.cs
  38. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleActionHandler.cs
  39. 5
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs
  40. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs
  41. 35
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs
  42. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleRegistry.cs
  43. 89
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs
  44. 26
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs
  45. 2
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs
  46. 77
      backend/src/Squidex.Domain.Apps.Entities/Comments/CommentTriggerHandler.cs
  47. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs
  48. 5
      backend/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs
  49. 2
      backend/src/Squidex.Domain.Apps.Entities/Rules/ManualTriggerHandler.cs
  50. 4
      backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs
  51. 2
      backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs
  52. 2
      backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaChangedTriggerHandler.cs
  53. 5
      backend/src/Squidex.Infrastructure/Queries/Query.cs
  54. 5
      backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs
  55. 26
      backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/CommentRuleTriggerDto.cs
  56. 4
      backend/src/Squidex/Config/Domain/RuleServices.cs
  57. 14
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs
  58. 121
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs
  59. 15
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs
  60. 293
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentTriggerHandlerTests.cs
  61. 19
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs
  62. 7
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/ManualTriggerHandlerTests.cs
  63. 12
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs
  64. 11
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/UsageTracking/UsageTriggerHandlerTests.cs
  65. 11
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaChangedTriggerHandlerTests.cs
  66. 2
      backend/tools/Migrate_01/OldTriggers/AssetChangedTrigger.cs
  67. 2
      backend/tools/Migrate_01/OldTriggers/ContentChangedTriggerSchema.cs
  68. 1
      frontend/app/features/rules/declarations.ts
  69. 2
      frontend/app/features/rules/module.ts
  70. 2
      frontend/app/features/rules/pages/rules/rule-element.component.scss
  71. 7
      frontend/app/features/rules/pages/rules/rule-wizard.component.html
  72. 28
      frontend/app/features/rules/pages/rules/triggers/comment-trigger.component.html
  73. 6
      frontend/app/features/rules/pages/rules/triggers/comment-trigger.component.scss
  74. 30
      frontend/app/features/rules/pages/rules/triggers/comment-trigger.component.ts
  75. 2
      frontend/app/features/settings/pages/contributors/contributor-add-form.component.ts
  76. 8
      frontend/app/features/settings/pages/roles/role.component.ts
  77. 6
      frontend/app/framework/angular/pipes/markdown.pipe.ts
  78. 16
      frontend/app/shared/components/comment.component.html
  79. 2
      frontend/app/shared/components/comment.component.scss
  80. 4
      frontend/app/shared/services/clients.service.spec.ts
  81. 2
      frontend/app/shared/services/contributors.service.spec.ts
  82. 8
      frontend/app/shared/services/rules.service.spec.ts
  83. 18
      frontend/app/shared/services/rules.service.ts
  84. 10
      frontend/app/shared/services/schemas.service.spec.ts
  85. 2
      frontend/app/theme/_bootstrap.scss

2
backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaAction.cs

@ -16,7 +16,7 @@ namespace Squidex.Extensions.Actions.Algolia
IconImage = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><path d='M16 .842C7.633.842.842 7.625.842 16S7.625 31.158 16 31.158c8.374 0 15.158-6.791 15.158-15.166S24.375.842 16 .842zm0 25.83c-5.898 0-10.68-4.781-10.68-10.68S10.101 5.313 16 5.313s10.68 4.781 10.68 10.679-4.781 10.68-10.68 10.68zm0-19.156v7.956c0 .233.249.388.458.279l7.055-3.663a.312.312 0 0 0 .124-.434 8.807 8.807 0 0 0-7.319-4.447z'/></svg>",
IconColor = "#0d9bf9",
Display = "Populate Algolia index",
Description = "Populate and synchronize indexes in Algolia for full text search.",
Description = "Populate a full text search index in Algolia.",
ReadMore = "https://www.algolia.com/")]
public sealed class AlgoliaAction : RuleAction
{

2
backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs

@ -12,7 +12,7 @@ using Algolia.Search;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using AlgoliaIndex = Algolia.Search.Index;
#pragma warning disable IDE0059 // Value assigned to symbol is never used

2
backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueActionHandler.cs

@ -10,7 +10,7 @@ using System.Threading.Tasks;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Queue;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
namespace Squidex.Extensions.Actions.AzureQueue
{

2
backend/extensions/Squidex.Extensions/Actions/Comment/CommentActionHandler.cs

@ -9,7 +9,7 @@ using System;
using System.Threading;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Entities.Comments.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;

2
backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs

@ -11,7 +11,7 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
namespace Squidex.Extensions.Actions.Discourse
{

2
backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchAction.cs

@ -18,7 +18,7 @@ namespace Squidex.Extensions.Actions.ElasticSearch
IconImage = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 29 28'><path d='M13.427 17.436H4.163C3.827 16.354 3.636 15.2 3.636 14s.182-2.355.527-3.436h15.245c1.891 0 3.418 1.545 3.418 3.445a3.421 3.421 0 0 1-3.418 3.427h-5.982zm-.436 1.146H4.6a11.508 11.508 0 0 0 4.2 4.982 11.443 11.443 0 0 0 15.827-3.209 5.793 5.793 0 0 0-4.173-1.773H12.99zm7.464-9.164a5.794 5.794 0 0 0 4.173-1.773 11.45 11.45 0 0 0-9.536-5.1c-2.327 0-4.491.7-6.3 1.891a11.554 11.554 0 0 0-4.2 4.982h15.864z'/></svg>",
IconColor = "#1e5470",
Display = "Populate Elasticsearch index",
Description = "Populate and synchronize indexes in ElasticSearch for full text search.",
Description = "Populate a full text search index in ElasticSearch.",
ReadMore = "https://www.elastic.co/")]
public sealed class ElasticSearchAction : RuleAction
{

2
backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs

@ -10,7 +10,7 @@ using System.Threading;
using System.Threading.Tasks;
using Elasticsearch.Net;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
#pragma warning disable IDE0059 // Value assigned to symbol is never used

2
backend/extensions/Squidex.Extensions/Actions/Email/EmailActionHandler.cs

@ -10,7 +10,7 @@ using System.Net.Mail;
using System.Threading;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
namespace Squidex.Extensions.Actions.Email
{

2
backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyActionHandler.cs

@ -10,7 +10,7 @@ using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Infrastructure;
namespace Squidex.Extensions.Actions.Fastly

2
backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaActionHandler.cs

@ -9,7 +9,7 @@ using System;
using System.Threading;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
namespace Squidex.Extensions.Actions.Kafka
{

2
backend/extensions/Squidex.Extensions/Actions/Medium/MediumActionHandler.cs

@ -11,7 +11,7 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Infrastructure.Http;
using Squidex.Infrastructure.Json;

2
backend/extensions/Squidex.Extensions/Actions/Notification/NotificationActionHandler.cs

@ -9,7 +9,7 @@ using System;
using System.Threading;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Entities.Comments.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;

2
backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderAction.cs

@ -16,7 +16,7 @@ namespace Squidex.Extensions.Actions.Prerender
IconImage = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><path d='M2.073 17.984l8.646-5.36v-1.787L.356 17.325v1.318l10.363 6.488v-1.787zM29.927 17.984l-8.646-5.36v-1.787l10.363 6.488v1.318l-10.363 6.488v-1.787zM18.228 6.693l-6.276 19.426 1.656.548 6.276-19.426z'/></svg>",
IconColor = "#2c3e50",
Display = "Recache URL",
Description = "Recache a certain URL or cache a URL for the first time.",
Description = "Prerender a javascript website for bots.",
ReadMore = "https://prerender.io")]
public sealed class PrerenderAction : RuleAction
{

2
backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderActionHandler.cs

@ -10,7 +10,7 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
namespace Squidex.Extensions.Actions.Prerender
{

2
backend/extensions/Squidex.Extensions/Actions/Slack/SlackAction.cs

@ -18,7 +18,7 @@ namespace Squidex.Extensions.Actions.Slack
IconImage = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 26 28'><path d='M23.734 12.125c1.281 0 2.266.938 2.266 2.219 0 1-.516 1.703-1.453 2.031l-2.688.922.875 2.609c.078.234.109.484.109.734 0 1.234-1 2.266-2.234 2.266a2.271 2.271 0 0 1-2.172-1.547l-.859-2.578-4.844 1.656.859 2.562c.078.234.125.484.125.734 0 1.219-1 2.266-2.25 2.266a2.25 2.25 0 0 1-2.156-1.547l-.859-2.547-2.391.828c-.25.078-.516.141-.781.141-1.266 0-2.219-.938-2.219-2.203 0-.969.625-1.844 1.547-2.156l2.438-.828-1.641-4.891-2.438.844c-.25.078-.5.125-.75.125-1.25 0-2.219-.953-2.219-2.203 0-.969.625-1.844 1.547-2.156l2.453-.828-.828-2.484a2.337 2.337 0 0 1-.125-.734c0-1.234 1-2.266 2.25-2.266a2.25 2.25 0 0 1 2.156 1.547l.844 2.5L13.14 5.5 12.296 3a2.337 2.337 0 0 1-.125-.734c0-1.234 1.016-2.266 2.25-2.266.984 0 1.859.625 2.172 1.547l.828 2.516 2.531-.859c.219-.063.438-.094.672-.094 1.219 0 2.266.906 2.266 2.156 0 .969-.75 1.781-1.625 2.078l-2.453.844 1.641 4.937 2.562-.875a2.32 2.32 0 0 1 .719-.125zm-12.406 4.094l4.844-1.641-1.641-4.922-4.844 1.672z'/></svg>",
IconColor = "#5c3a58",
Display = "Send to Slack",
Description = "Create a status update at slack to a channel you define.",
Description = "Create a status update to a slack channel.",
ReadMore = "https://slack.com")]
public sealed class SlackAction : RuleAction
{

2
backend/extensions/Squidex.Extensions/Actions/Slack/SlackActionHandler.cs

@ -11,7 +11,7 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Infrastructure;
namespace Squidex.Extensions.Actions.Slack

2
backend/extensions/Squidex.Extensions/Actions/Twitter/TweetAction.cs

@ -16,7 +16,7 @@ namespace Squidex.Extensions.Actions.Twitter
IconImage = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><path d='M32 7.075a12.941 12.941 0 0 1-3.769 1.031 6.601 6.601 0 0 0 2.887-3.631 13.21 13.21 0 0 1-4.169 1.594A6.565 6.565 0 0 0 22.155 4a6.563 6.563 0 0 0-6.563 6.563c0 .512.056 1.012.169 1.494A18.635 18.635 0 0 1 2.23 5.195a6.56 6.56 0 0 0-.887 3.3 6.557 6.557 0 0 0 2.919 5.463 6.565 6.565 0 0 1-2.975-.819v.081a6.565 6.565 0 0 0 5.269 6.437 6.574 6.574 0 0 1-2.968.112 6.588 6.588 0 0 0 6.131 4.563 13.17 13.17 0 0 1-9.725 2.719 18.568 18.568 0 0 0 10.069 2.95c12.075 0 18.681-10.006 18.681-18.681 0-.287-.006-.569-.019-.85A13.216 13.216 0 0 0 32 7.076z'/></svg>",
IconColor = "#1da1f2",
Display = "Tweet",
Description = "Create a status update at Tweet to a your user account.",
Description = "Tweet an update with your twitter account.",
ReadMore = "https://twitter.com")]
public sealed class TweetAction : RuleAction
{

2
backend/extensions/Squidex.Extensions/Actions/Twitter/TweetActionHandler.cs

@ -11,7 +11,7 @@ using System.Threading.Tasks;
using CoreTweet;
using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Infrastructure;
namespace Squidex.Extensions.Actions.Twitter

2
backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookAction.cs

@ -17,7 +17,7 @@ namespace Squidex.Extensions.Actions.Webhook
IconImage = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 28 28'><path d='M5.95 27.125h-.262C1.75 26.425 0 23.187 0 20.3c0-2.713 1.575-5.688 5.075-6.563V9.712c0-.525.35-.875.875-.875s.875.35.875.875v4.725c0 .438-.35.787-.7.875-2.975.438-4.375 2.8-4.375 4.988s1.313 4.55 4.2 5.075h.175a.907.907 0 0 1 .7 1.05c-.088.438-.438.7-.875.7zM21.175 27.387c-2.8 0-5.775-1.662-6.65-5.075H9.712c-.525 0-.875-.35-.875-.875s.35-.875.875-.875h5.512c.438 0 .787.35.875.7.438 2.975 2.8 4.288 4.988 4.375 2.188 0 4.55-1.313 5.075-4.2v-.088a.908.908 0 0 1 1.05-.7.908.908 0 0 1 .7 1.05v.088c-.612 3.85-3.85 5.6-6.737 5.6zM21.525 18.55c-.525 0-.875-.35-.875-.875v-4.813c0-.438.35-.787.7-.875 2.975-.438 4.288-2.8 4.375-4.987 0-2.188-1.313-4.55-4.2-5.075h-.088c-.525-.175-.875-.613-.787-1.05s.525-.788 1.05-.7h.088c3.938.7 5.688 3.937 5.688 6.825 0 2.713-1.662 5.688-5.075 6.563v4.113c0 .438-.438.875-.875.875zM1.137 6.737H.962c-.438-.087-.788-.525-.7-.963v-.087c.7-3.938 3.85-5.688 6.737-5.688h.087c2.712 0 5.688 1.662 6.563 5.075h4.025c.525 0 .875.35.875.875s-.35.875-.875.875h-4.725c-.438 0-.788-.35-.875-.7-.438-2.975-2.8-4.288-4.988-4.375-2.188 0-4.55 1.313-5.075 4.2v.087c-.088.438-.438.7-.875.7z'/><path d='M7 10.588c-.875 0-1.837-.35-2.538-1.05a3.591 3.591 0 0 1 0-5.075C5.162 3.851 6.037 3.5 7 3.5s1.838.35 2.537 1.05c.7.7 1.05 1.575 1.05 2.537s-.35 1.837-1.05 2.538c-.7.612-1.575.963-2.537.963zM7 5.25c-.438 0-.875.175-1.225.525a1.795 1.795 0 0 0 2.538 2.538c.35-.35.525-.788.525-1.313s-.175-.875-.525-1.225S7.525 5.25 7 5.25zM21.088 23.887a3.65 3.65 0 0 1-2.537-1.05 3.591 3.591 0 0 1 0-5.075c.7-.7 1.575-1.05 2.537-1.05s1.838.35 2.537 1.05c.7.7 1.05 1.575 1.05 2.538s-.35 1.837-1.05 2.537c-.787.7-1.662 1.05-2.537 1.05zm0-5.337c-.525 0-.963.175-1.313.525a1.795 1.795 0 0 0 2.537 2.538c.35-.35.525-.788.525-1.313s-.175-.963-.525-1.313-.787-.438-1.225-.438zM20.387 10.588c-.875 0-1.837-.35-2.537-1.05S16.8 7.963 16.8 7.001s.35-1.837 1.05-2.538c.7-.612 1.662-.962 2.537-.962s1.838.35 2.538 1.05c1.4 1.4 1.4 3.675 0 5.075-.7.612-1.575.963-2.538.963zm0-5.338c-.525 0-.962.175-1.313.525s-.525.788-.525 1.313.175.962.525 1.313c.7.7 1.838.7 2.538 0s.7-1.838 0-2.538c-.263-.438-.7-.612-1.225-.612zM7.087 23.887c-.875 0-1.837-.35-2.538-1.05s-1.05-1.575-1.05-2.537.35-1.838 1.05-2.538c.7-.612 1.575-.962 2.538-.962s1.837.35 2.538 1.05c1.4 1.4 1.4 3.675 0 5.075-.7.612-1.575.962-2.538.962zm0-5.337c-.525 0-.962.175-1.313.525s-.525.788-.525 1.313.175.963.525 1.313a1.794 1.794 0 1 0 2.538-2.537c-.263-.438-.7-.612-1.225-.612z'/></svg>",
IconColor = "#4bb958",
Display = "Send webhook",
Description = "Send events like ContentPublished to your webhook.",
Description = "Invoke HTTP endpoints on a target system.",
ReadMore = "https://en.wikipedia.org/wiki/Webhook")]
public sealed class WebhookAction : RuleAction
{

2
backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs

@ -11,7 +11,7 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Infrastructure;
namespace Squidex.Extensions.Actions.Webhook

2
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedAssetEvent.cs → backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedAssetEvent.cs

@ -9,7 +9,7 @@ using System;
using NodaTime;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents
namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents
{
public sealed class EnrichedAssetEvent : EnrichedUserEventBase, IEnrichedEntityEvent
{

2
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedAssetEventType.cs → backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedAssetEventType.cs

@ -5,7 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents
namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents
{
public enum EnrichedAssetEventType
{

28
backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedCommentEvent.cs

@ -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(); }
}
}
}

2
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEvent.cs → backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedContentEvent.cs

@ -10,7 +10,7 @@ using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents
namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents
{
public sealed class EnrichedContentEvent : EnrichedSchemaEventBase, IEnrichedEntityEvent
{

2
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEventType.cs → backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedContentEventType.cs

@ -5,7 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents
namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents
{
public enum EnrichedContentEventType
{

2
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedEvent.cs → backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedEvent.cs

@ -9,7 +9,7 @@ using System;
using NodaTime;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents
namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents
{
public abstract class EnrichedEvent
{

2
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedManualEvent.cs → backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedManualEvent.cs

@ -5,7 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents
namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents
{
public sealed class EnrichedManualEvent : EnrichedEvent
{

2
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedSchemaEvent.cs → backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedSchemaEvent.cs

@ -7,7 +7,7 @@
using System;
namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents
namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents
{
public sealed class EnrichedSchemaEvent : EnrichedSchemaEventBase, IEnrichedEntityEvent
{

2
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedSchemaEventBase.cs → backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedSchemaEventBase.cs

@ -8,7 +8,7 @@
using System;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents
namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents
{
public abstract class EnrichedSchemaEventBase : EnrichedUserEventBase
{

2
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedSchemaEventType.cs → backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedSchemaEventType.cs

@ -5,7 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents
namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents
{
public enum EnrichedSchemaEventType
{

2
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUsageExceededEvent.cs → backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedUsageExceededEvent.cs

@ -5,7 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents
namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents
{
public sealed class EnrichedUsageExceededEvent : EnrichedEvent
{

2
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUserEventBase.cs → backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedUserEventBase.cs

@ -9,7 +9,7 @@ using System.Runtime.Serialization;
using Squidex.Infrastructure;
using Squidex.Shared.Users;
namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents
namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents
{
public abstract class EnrichedUserEventBase : EnrichedEvent
{

2
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/IEnrichedEntityEvent.cs → backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/IEnrichedEntityEvent.cs

@ -7,7 +7,7 @@
using System;
namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents
namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents
{
public interface IEnrichedEntityEvent
{

2
backend/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs

@ -15,6 +15,8 @@ namespace Squidex.Domain.Apps.Core.Rules
T Visit(ContentChangedTriggerV2 trigger);
T Visit(CommentTrigger trigger);
T Visit(ManualTrigger trigger);
T Visit(SchemaChangedTrigger trigger);

22
backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/CommentTrigger.cs

@ -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);
}
}
}

2
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventEnricher.cs

@ -8,7 +8,7 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;

2
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IEventEnricher.cs

@ -6,7 +6,7 @@
// ==========================================================================
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure.EventSourcing;

2
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleActionHandler.cs

@ -8,8 +8,8 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
namespace Squidex.Domain.Apps.Core.HandleRules
{

5
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs

@ -6,9 +6,10 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure.EventSourcing;
@ -18,7 +19,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules
{
Type TriggerType { get; }
Task<EnrichedEvent?> CreateEnrichedEventAsync(Envelope<AppEvent> @event);
Task<List<EnrichedEvent>> CreateEnrichedEventsAsync(Envelope<AppEvent> @event);
bool Trigger(EnrichedEvent @event, RuleTrigger trigger);

2
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs

@ -8,8 +8,8 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Infrastructure;
#pragma warning disable RECS0083 // Shows NotImplementedException throws in the quick task bar

35
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs

@ -12,7 +12,7 @@ using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json;
@ -50,6 +50,9 @@ namespace Squidex.Domain.Apps.Core.HandleRules
AddPattern("CONTENT_ACTION", ContentAction);
AddPattern("CONTENT_STATUS", ContentStatus);
AddPattern("CONTENT_URL", ContentUrl);
AddPattern("MENTIONED_ID", MentionedId);
AddPattern("MENTIONED_NAME", MentionedName);
AddPattern("MENTIONED_EMAIL", MentionedEmail);
AddPattern("SCHEMA_ID", SchemaId);
AddPattern("SCHEMA_NAME", SchemaName);
AddPattern("TIMESTAMP_DATETIME", TimestampTime);
@ -266,6 +269,36 @@ namespace Squidex.Domain.Apps.Core.HandleRules
return Fallback;
}
private static string MentionedName(EnrichedEvent @event)
{
if (@event is EnrichedCommentEvent commentEvent)
{
return commentEvent.MentionedUser.DisplayName() ?? Fallback;
}
return Fallback;
}
private static string MentionedId(EnrichedEvent @event)
{
if (@event is EnrichedCommentEvent commentEvent)
{
return commentEvent.MentionedUser.Id ?? Fallback;
}
return Fallback;
}
private static string MentionedEmail(EnrichedEvent @event)
{
if (@event is EnrichedCommentEvent commentEvent)
{
return commentEvent.MentionedUser.Email ?? Fallback;
}
return Fallback;
}
private static string CalculateData(NamedContentData data, Match match)
{
var captures = match.Groups[2].Captures;

2
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleRegistry.cs

@ -10,8 +10,8 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection;

89
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs

@ -67,21 +67,23 @@ namespace Squidex.Domain.Apps.Core.HandleRules
this.log = log;
}
public virtual async Task<RuleJob?> CreateJobAsync(Rule rule, Guid ruleId, Envelope<IEvent> @event)
public virtual async Task<List<RuleJob>> CreateJobsAsync(Rule rule, Guid ruleId, Envelope<IEvent> @event)
{
Guard.NotNull(rule);
Guard.NotNull(@event);
var result = new List<RuleJob>();
try
{
if (!rule.IsEnabled)
{
return null;
return result;
}
if (!(@event.Payload is AppEvent))
{
return null;
return result;
}
var typed = @event.To<AppEvent>();
@ -90,12 +92,12 @@ namespace Squidex.Domain.Apps.Core.HandleRules
if (!ruleTriggerHandlers.TryGetValue(rule.Trigger.GetType(), out var triggerHandler))
{
return null;
return result;
}
if (!ruleActionHandlers.TryGetValue(actionType, out var actionHandler))
{
return null;
return result;
}
var now = clock.GetCurrentInstant();
@ -109,59 +111,66 @@ namespace Squidex.Domain.Apps.Core.HandleRules
if (eventTime.Plus(Constants.StaleTime) < now)
{
return null;
return result;
}
if (!triggerHandler.Trigger(typed.Payload, rule.Trigger, ruleId))
{
return null;
return result;
}
var appEventEnvelope = @event.To<AppEvent>();
var enrichedEvent = await triggerHandler.CreateEnrichedEventAsync(appEventEnvelope);
if (enrichedEvent == null)
{
return null;
}
await eventEnricher.EnrichAsync(enrichedEvent, typed);
var enrichedEvents = await triggerHandler.CreateEnrichedEventsAsync(appEventEnvelope);
if (!triggerHandler.Trigger(enrichedEvent, rule.Trigger))
foreach (var enrichedEvent in enrichedEvents)
{
return null;
try
{
await eventEnricher.EnrichAsync(enrichedEvent, typed);
if (!triggerHandler.Trigger(enrichedEvent, rule.Trigger))
{
continue;
}
var actionName = typeNameRegistry.GetName(actionType);
var actionData = await actionHandler.CreateJobAsync(enrichedEvent, rule.Action);
var json = jsonSerializer.Serialize(actionData.Data);
var job = new RuleJob
{
Id = Guid.NewGuid(),
ActionData = json,
ActionName = actionName,
AppId = enrichedEvent.AppId.Id,
Created = now,
Description = actionData.Description,
EventName = enrichedEvent.Name,
ExecutionPartition = enrichedEvent.Partition,
Expires = expires,
RuleId = ruleId
};
result.Add(job);
}
catch (Exception ex)
{
log.LogError(ex, w => w
.WriteProperty("action", "createRuleJobFromEvent")
.WriteProperty("status", "Failed"));
}
}
var actionName = typeNameRegistry.GetName(actionType);
var actionData = await actionHandler.CreateJobAsync(enrichedEvent, rule.Action);
var json = jsonSerializer.Serialize(actionData.Data);
var job = new RuleJob
{
Id = Guid.NewGuid(),
ActionData = json,
ActionName = actionName,
AppId = enrichedEvent.AppId.Id,
Created = now,
Description = actionData.Description,
EventName = enrichedEvent.Name,
ExecutionPartition = enrichedEvent.Partition,
Expires = expires,
RuleId = ruleId
};
return job;
}
catch (Exception ex)
{
log.LogError(ex, w => w
.WriteProperty("action", "createRuleJob")
.WriteProperty("status", "Failed"));
return null;
}
return result;
}
public virtual async Task<(Result Result, TimeSpan Elapsed)> InvokeAsync(string actionName, string job)

26
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs

@ -6,9 +6,10 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure.EventSourcing;
@ -21,14 +22,28 @@ namespace Squidex.Domain.Apps.Core.HandleRules
where TEvent : AppEvent
where TEnrichedEvent : EnrichedEvent
{
private readonly List<EnrichedEvent> emptyEnrichedEvents = new List<EnrichedEvent>();
public Type TriggerType
{
get { return typeof(TTrigger); }
}
async Task<EnrichedEvent?> IRuleTriggerHandler.CreateEnrichedEventAsync(Envelope<AppEvent> @event)
public virtual async Task<List<EnrichedEvent>> CreateEnrichedEventsAsync(Envelope<AppEvent> @event)
{
return await CreateEnrichedEventAsync(@event.To<TEvent>());
var enrichedEvent = await CreateEnrichedEventAsync(@event.To<TEvent>());
if (enrichedEvent != null)
{
return new List<EnrichedEvent>
{
enrichedEvent
};
}
else
{
return emptyEnrichedEvents;
}
}
bool IRuleTriggerHandler.Trigger(EnrichedEvent @event, RuleTrigger trigger)
@ -51,7 +66,10 @@ namespace Squidex.Domain.Apps.Core.HandleRules
return false;
}
protected abstract Task<TEnrichedEvent?> CreateEnrichedEventAsync(Envelope<TEvent> @event);
protected virtual Task<TEnrichedEvent?> CreateEnrichedEventAsync(Envelope<TEvent> @event)
{
return Task.FromResult<TEnrichedEvent?>(null);
}
protected abstract bool Trigger(TEnrichedEvent @event, TTrigger trigger);

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

@ -7,7 +7,7 @@
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
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.Assets;

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

@ -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);
}
}
}

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

@ -9,7 +9,7 @@ using System;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
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.Contents;

5
backend/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs

@ -36,6 +36,11 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards
return action.Accept(visitor);
}
public Task<IEnumerable<ValidationError>> Visit(CommentTrigger trigger)
{
return Task.FromResult(Enumerable.Empty<ValidationError>());
}
public Task<IEnumerable<ValidationError>> Visit(AssetChangedTriggerV2 trigger)
{
return Task.FromResult(Enumerable.Empty<ValidationError>());

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

@ -7,7 +7,7 @@
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Domain.Apps.Events.Rules;
using Squidex.Infrastructure.EventSourcing;

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

@ -68,9 +68,9 @@ namespace Squidex.Domain.Apps.Entities.Rules
Guard.NotNull(rule);
Guard.NotNull(@event);
var job = await ruleService.CreateJobAsync(rule, ruleId, @event);
var jobs = await ruleService.CreateJobsAsync(rule, ruleId, @event);
if (job != null)
foreach (var job in jobs)
{
await ruleEventRepository.EnqueueAsync(job, job.Created);
}

2
backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs

@ -7,7 +7,7 @@
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure.EventSourcing;

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

@ -7,7 +7,7 @@
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
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;

5
backend/src/Squidex.Infrastructure/Queries/Query.cs

@ -19,6 +19,11 @@ namespace Squidex.Infrastructure.Queries
public long Take { get; set; } = long.MaxValue;
public long Top
{
set { Take = value; }
}
public List<SortNode> Sort { get; set; } = new List<SortNode>();
public override string ToString()

5
backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs

@ -31,6 +31,11 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models.Converters
return SimpleMapper.Map(trigger, new AssetChangedRuleTriggerDto());
}
public RuleTriggerDto Visit(CommentTrigger trigger)
{
return SimpleMapper.Map(trigger, new CommentRuleTriggerDto());
}
public RuleTriggerDto Visit(ManualTrigger trigger)
{
return SimpleMapper.Map(trigger, new ManualRuleTriggerDto());

26
backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/CommentRuleTriggerDto.cs

@ -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());
}
}
}

4
backend/src/Squidex/Config/Domain/RuleServices.cs

@ -9,6 +9,7 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Comments;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Rules;
using Squidex.Domain.Apps.Entities.Rules.Queries;
@ -33,6 +34,9 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<AssetChangedTriggerHandler>()
.As<IRuleTriggerHandler>();
services.AddSingletonAs<CommentTriggerHandler>()
.As<IRuleTriggerHandler>();
services.AddSingletonAs<ContentChangedTriggerHandler>()
.As<IRuleTriggerHandler>();

14
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs

@ -12,7 +12,7 @@ using FakeItEasy;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
@ -113,6 +113,18 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
Assert.Equal($"Date: {now:yyyy-MM-dd}, Full: {now:yyyy-MM-dd-hh-mm-ss}", result);
}
[Theory]
[InlineData("From $MENTIONED_NAME ($MENTIONED_EMAIL, $MENTIONED_ID)")]
[InlineData("Script(`From ${event.mentionedUser.name} (${event.mentionedUser.email}, ${event.mentionedUser.id})`)")]
public void Should_format_email_and_display_name_from_mentioned_user(string script)
{
var @event = new EnrichedCommentEvent { MentionedUser = user };
var result = sut.Format(script, @event);
Assert.Equal("From me (me@email.com, 123)", result);
}
[Theory]
[InlineData("From $USER_NAME ($USER_EMAIL, $USER_ID)")]
[InlineData("Script(`From ${event.user.name} (${event.user.email}, ${event.user.id})`)")]

121
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs

@ -6,14 +6,16 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using FakeItEasy;
using Microsoft.Extensions.Options;
using NodaTime;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Contents;
@ -97,9 +99,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
{
var @event = Envelope.Create(new ContentCreated());
var job = await sut.CreateJobAsync(ValidRule().Disable(), ruleId, @event);
var jobs = await sut.CreateJobsAsync(ValidRule().Disable(), ruleId, @event);
Assert.Null(job);
Assert.Empty(jobs);
A.CallTo(() => ruleTriggerHandler.Trigger(A<AppEvent>.Ignored, A<RuleTrigger>.Ignored, ruleId))
.MustNotHaveHappened();
@ -110,9 +112,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
{
var @event = Envelope.Create(new InvalidEvent());
var job = await sut.CreateJobAsync(ValidRule(), ruleId, @event);
var jobs = await sut.CreateJobsAsync(ValidRule(), ruleId, @event);
Assert.Null(job);
Assert.Empty(jobs);
A.CallTo(() => ruleTriggerHandler.Trigger(A<AppEvent>.Ignored, A<RuleTrigger>.Ignored, ruleId))
.MustNotHaveHappened();
@ -123,9 +125,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
{
var @event = Envelope.Create(new ContentCreated());
var job = await sut.CreateJobAsync(RuleInvalidTrigger(), ruleId, @event);
var jobs = await sut.CreateJobsAsync(RuleInvalidTrigger(), ruleId, @event);
Assert.Null(job);
Assert.Empty(jobs);
A.CallTo(() => ruleTriggerHandler.Trigger(A<AppEvent>.Ignored, A<RuleTrigger>.Ignored, ruleId))
.MustNotHaveHappened();
@ -136,9 +138,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
{
var @event = Envelope.Create(new ContentCreated());
var job = await sut.CreateJobAsync(RuleInvalidAction(), ruleId, @event);
var jobs = await sut.CreateJobsAsync(RuleInvalidAction(), ruleId, @event);
Assert.Null(job);
Assert.Empty(jobs);
A.CallTo(() => ruleTriggerHandler.Trigger(A<AppEvent>.Ignored, A<RuleTrigger>.Ignored, ruleId))
.MustNotHaveHappened();
@ -149,9 +151,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
{
var @event = Envelope.Create(new ContentCreated()).SetTimestamp(clock.GetCurrentInstant().Minus(Duration.FromDays(3)));
var job = await sut.CreateJobAsync(ValidRule(), ruleId, @event);
var jobs = await sut.CreateJobsAsync(ValidRule(), ruleId, @event);
Assert.Null(job);
Assert.Empty(jobs);
A.CallTo(() => ruleTriggerHandler.Trigger(A<AppEvent>.Ignored, A<RuleTrigger>.Ignored, ruleId))
.MustNotHaveHappened();
@ -167,11 +169,11 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
A.CallTo(() => ruleTriggerHandler.Trigger(@event.Payload, rule.Trigger, ruleId))
.Returns(false);
var job = await sut.CreateJobAsync(rule, ruleId, @event);
var jobs = await sut.CreateJobsAsync(rule, ruleId, @event);
Assert.Null(job);
Assert.Empty(jobs);
A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventAsync(A<Envelope<AppEvent>>.Ignored))
A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventsAsync(A<Envelope<AppEvent>>.Ignored))
.MustNotHaveHappened();
}
@ -185,12 +187,12 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
A.CallTo(() => ruleTriggerHandler.Trigger(@event.Payload, rule.Trigger, ruleId))
.Returns(true);
A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventAsync(A<Envelope<AppEvent>>.That.Matches(x => x.Payload == @event.Payload)))
.Returns(Task.FromResult<EnrichedEvent?>(null));
A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventsAsync(A<Envelope<AppEvent>>.That.Matches(x => x.Payload == @event.Payload)))
.Returns(new List<EnrichedEvent>());
var job = await sut.CreateJobAsync(rule, ruleId, @event);
var jobs = await sut.CreateJobsAsync(rule, ruleId, @event);
Assert.Null(job);
Assert.Empty(jobs);
}
[Fact]
@ -205,15 +207,15 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
A.CallTo(() => ruleTriggerHandler.Trigger(@event.Payload, rule.Trigger, ruleId))
.Returns(true);
A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventAsync(A<Envelope<AppEvent>>.That.Matches(x => x.Payload == @event.Payload)))
.Returns(enrichedEvent);
A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventsAsync(A<Envelope<AppEvent>>.That.Matches(x => x.Payload == @event.Payload)))
.Returns(new List<EnrichedEvent> { enrichedEvent });
A.CallTo(() => ruleTriggerHandler.Trigger(enrichedEvent, rule.Trigger))
.Returns(false);
var job = await sut.CreateJobAsync(rule, ruleId, @event);
var jobs = await sut.CreateJobsAsync(rule, ruleId, @event);
Assert.Null(job);
Assert.Empty(jobs);
}
[Fact]
@ -230,29 +232,64 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
A.CallTo(() => ruleTriggerHandler.Trigger(@event.Payload, rule.Trigger, ruleId))
.Returns(true);
A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventsAsync(A<Envelope<AppEvent>>.That.Matches(x => x.Payload == @event.Payload)))
.Returns(new List<EnrichedEvent> { enrichedEvent });
A.CallTo(() => ruleTriggerHandler.Trigger(enrichedEvent, rule.Trigger))
.Returns(true);
A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventAsync(A<Envelope<AppEvent>>.That.Matches(x => x.Payload == @event.Payload)))
.Returns(enrichedEvent);
A.CallTo(() => ruleActionHandler.CreateJobAsync(A<EnrichedEvent>.Ignored, rule.Action))
A.CallTo(() => ruleActionHandler.CreateJobAsync(enrichedEvent, rule.Action))
.Returns((actionDescription, new ValidData { Value = 10 }));
var job = (await sut.CreateJobAsync(rule, ruleId, @event))!;
var jobs = (await sut.CreateJobsAsync(rule, ruleId, @event))!;
Assert.Equal(actionData, job.ActionData);
Assert.Equal(actionName, job.ActionName);
Assert.Equal(actionDescription, job.Description);
var job = jobs.Single();
Assert.Equal(now, job.Created);
Assert.Equal(now.Plus(Duration.FromDays(30)), job.Expires);
AssertJob(now, enrichedEvent, job);
Assert.Equal(enrichedEvent.AppId.Id, job.AppId);
A.CallTo(() => eventEnricher.EnrichAsync(enrichedEvent, A<Envelope<AppEvent>>.That.Matches(x => x.Payload == @event.Payload)))
.MustHaveHappened();
}
Assert.NotEqual(Guid.Empty, job.Id);
[Fact]
public async Task Should_create_multiple_jobs_if_triggered()
{
var now = clock.GetCurrentInstant();
A.CallTo(() => eventEnricher.EnrichAsync(enrichedEvent, A<Envelope<AppEvent>>.That.Matches(x => x.Payload == @event.Payload)))
var rule = ValidRule();
var enrichedEvent1 = new EnrichedContentEvent { AppId = appId };
var enrichedEvent2 = new EnrichedContentEvent { AppId = appId };
var @event = Envelope.Create(new ContentCreated()).SetTimestamp(now);
A.CallTo(() => ruleTriggerHandler.Trigger(@event.Payload, rule.Trigger, ruleId))
.Returns(true);
A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventsAsync(A<Envelope<AppEvent>>.That.Matches(x => x.Payload == @event.Payload)))
.Returns(new List<EnrichedEvent> { enrichedEvent1, enrichedEvent2 });
A.CallTo(() => ruleTriggerHandler.Trigger(enrichedEvent1, rule.Trigger))
.Returns(true);
A.CallTo(() => ruleTriggerHandler.Trigger(enrichedEvent2, rule.Trigger))
.Returns(true);
A.CallTo(() => ruleActionHandler.CreateJobAsync(enrichedEvent1, rule.Action))
.Returns((actionDescription, new ValidData { Value = 10 }));
A.CallTo(() => ruleActionHandler.CreateJobAsync(enrichedEvent2, rule.Action))
.Returns((actionDescription, new ValidData { Value = 10 }));
var jobs = (await sut.CreateJobsAsync(rule, ruleId, @event))!;
AssertJob(now, enrichedEvent1, jobs[0]);
AssertJob(now, enrichedEvent1, jobs[1]);
A.CallTo(() => eventEnricher.EnrichAsync(enrichedEvent1, A<Envelope<AppEvent>>.That.Matches(x => x.Payload == @event.Payload)))
.MustHaveHappened();
A.CallTo(() => eventEnricher.EnrichAsync(enrichedEvent2, A<Envelope<AppEvent>>.That.Matches(x => x.Payload == @event.Payload)))
.MustHaveHappened();
}
@ -327,5 +364,19 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
{
return new Rule(new ContentChangedTriggerV2(), new ValidAction());
}
private void AssertJob(Instant now, EnrichedContentEvent enrichedEvent, RuleJob job)
{
Assert.Equal(enrichedEvent.AppId.Id, job.AppId);
Assert.Equal(actionData, job.ActionData);
Assert.Equal(actionName, job.ActionName);
Assert.Equal(actionDescription, job.Description);
Assert.Equal(now, job.Created);
Assert.Equal(now.Plus(Duration.FromDays(30)), job.Expires);
Assert.NotEqual(Guid.Empty, job.Id);
}
}
}

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

@ -7,10 +7,11 @@
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.HandleRules.EnrichedEvents;
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;
@ -50,16 +51,18 @@ namespace Squidex.Domain.Apps.Entities.Assets
[Theory]
[MemberData(nameof(TestEvents))]
public async Task Should_enrich_events(AssetEvent @event, EnrichedAssetEventType type)
public async Task Should_create_enriched_events(AssetEvent @event, EnrichedAssetEventType type)
{
var envelope = Envelope.Create<AppEvent>(@event).SetEventStreamNumber(12);
A.CallTo(() => assetLoader.GetAsync(@event.AssetId, 12))
.Returns(new AssetEntity());
var result = await sut.CreateEnrichedEventAsync(envelope) as EnrichedAssetEvent;
var result = await sut.CreateEnrichedEventsAsync(envelope);
Assert.Equal(type, result!.Type);
var enrichedEvent = result.Single() as EnrichedAssetEvent;
Assert.Equal(type, enrichedEvent!.Type);
}
[Fact]
@ -67,9 +70,9 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
var envelope = Envelope.Create<AppEvent>(new AssetMoved());
var result = await sut.CreateEnrichedEventAsync(envelope);
var result = await sut.CreateEnrichedEventsAsync(envelope);
Assert.Null(result);
Assert.Empty(result);
}
[Fact]

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

@ -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();
}
}
}
}

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

@ -8,11 +8,12 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
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;
@ -59,16 +60,18 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Theory]
[MemberData(nameof(TestEvents))]
public async Task Should_enrich_events(ContentEvent @event, EnrichedContentEventType type)
public async Task Should_create_enriched_events(ContentEvent @event, EnrichedContentEventType type)
{
var envelope = Envelope.Create<AppEvent>(@event).SetEventStreamNumber(12);
A.CallTo(() => contentLoader.GetAsync(@event.ContentId, 12))
.Returns(new ContentEntity { SchemaId = SchemaMatch });
var result = await sut.CreateEnrichedEventAsync(envelope) as EnrichedContentEvent;
var result = await sut.CreateEnrichedEventsAsync(envelope);
Assert.Equal(type, result!.Type);
var enrichedEvent = result.Single() as EnrichedContentEvent;
Assert.Equal(type, enrichedEvent!.Type);
}
[Fact]
@ -87,10 +90,12 @@ namespace Squidex.Domain.Apps.Entities.Contents
A.CallTo(() => contentLoader.GetAsync(@event.ContentId, 11))
.Returns(new ContentEntity { SchemaId = SchemaMatch, Version = 11, Data = dataOld });
var result = await sut.CreateEnrichedEventAsync(envelope) as EnrichedContentEvent;
var result = await sut.CreateEnrichedEventsAsync(envelope);
var enrichedEvent = result.Single() as EnrichedContentEvent;
Assert.Same(dataNow, result!.Data);
Assert.Same(dataOld, result!.DataOld);
Assert.Same(dataNow, enrichedEvent!.Data);
Assert.Same(dataOld, enrichedEvent!.DataOld);
}
[Fact]

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

@ -5,9 +5,10 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Rules;
@ -25,9 +26,9 @@ namespace Squidex.Domain.Apps.Entities.Rules
{
var envelope = Envelope.Create<AppEvent>(new RuleManuallyTriggered());
var result = await sut.CreateEnrichedEventAsync(envelope);
var result = await sut.CreateEnrichedEventsAsync(envelope);
Assert.Equal("Manual", result!.Name);
Assert.Equal("Manual", result.Single().Name);
}
[Fact]

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

@ -74,8 +74,8 @@ namespace Squidex.Domain.Apps.Entities.Rules
var job = new RuleJob { Created = now };
A.CallTo(() => ruleService.CreateJobAsync(rule.RuleDef, rule.Id, @event))
.Returns(job);
A.CallTo(() => ruleService.CreateJobsAsync(rule.RuleDef, rule.Id, @event))
.Returns(new List<RuleJob> { job });
await sut.Enqueue(rule.RuleDef, rule.Id, @event);
@ -96,11 +96,11 @@ namespace Squidex.Domain.Apps.Entities.Rules
A.CallTo(() => appProvider.GetRulesAsync(appId.Id))
.Returns(new List<IRuleEntity> { rule1, rule2 });
A.CallTo(() => ruleService.CreateJobAsync(rule1.RuleDef, rule1.Id, @event))
.Returns(job1);
A.CallTo(() => ruleService.CreateJobsAsync(rule1.RuleDef, rule1.Id, @event))
.Returns(new List<RuleJob> { job1 });
A.CallTo(() => ruleService.CreateJobAsync(rule2.RuleDef, rule2.Id, @event))
.Returns(Task.FromResult<RuleJob?>(null));
A.CallTo(() => ruleService.CreateJobsAsync(rule2.RuleDef, rule2.Id, @event))
.Returns(new List<RuleJob>());
await sut.On(@event);

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

@ -6,9 +6,10 @@
// ==========================================================================
using System;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Contents;
@ -59,10 +60,12 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking
{
var @event = new AppUsageExceeded { CallsCurrent = 80, CallsLimit = 120 };
var result = await sut.CreateEnrichedEventAsync(Envelope.Create<AppEvent>(@event)) as EnrichedUsageExceededEvent;
var result = await sut.CreateEnrichedEventsAsync(Envelope.Create<AppEvent>(@event));
Assert.Equal(@event.CallsCurrent, result!.CallsCurrent);
Assert.Equal(@event.CallsLimit, result!.CallsLimit);
var enrichedEvent = result.Single() as EnrichedUsageExceededEvent;
Assert.Equal(@event.CallsCurrent, enrichedEvent!.CallsCurrent);
Assert.Equal(@event.CallsLimit, enrichedEvent!.CallsLimit);
}
}
}

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

@ -7,10 +7,11 @@
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.HandleRules.EnrichedEvents;
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;
@ -50,13 +51,15 @@ namespace Squidex.Domain.Apps.Entities.Schemas
[Theory]
[MemberData(nameof(TestEvents))]
public async Task Should_enrich_events(SchemaEvent @event, EnrichedSchemaEventType type)
public async Task Should_create_enriched_events(SchemaEvent @event, EnrichedSchemaEventType type)
{
var envelope = Envelope.Create<AppEvent>(@event).SetEventStreamNumber(12);
var result = await sut.CreateEnrichedEventAsync(envelope);
var result = await sut.CreateEnrichedEventsAsync(envelope);
Assert.Equal(type, ((EnrichedSchemaEvent)result!).Type);
var enrichedEvent = result.Single() as EnrichedSchemaEvent;
Assert.Equal(type, enrichedEvent!.Type);
}
[Fact]

2
backend/tools/Migrate_01/OldTriggers/AssetChangedTrigger.cs

@ -7,8 +7,8 @@
using System;
using System.Collections.Generic;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Infrastructure.Migrations;
using Squidex.Infrastructure.Reflection;

2
backend/tools/Migrate_01/OldTriggers/ContentChangedTriggerSchema.cs

@ -8,7 +8,7 @@
using System;
using System.Collections.Generic;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.Triggers;
namespace Migrate_01.OldTriggers

1
frontend/app/features/rules/declarations.ts

@ -8,6 +8,7 @@
export * from './pages/rules/actions/generic-action.component';
export * from './pages/rules/triggers/asset-changed-trigger.component';
export * from './pages/rules/triggers/comment-trigger.component';
export * from './pages/rules/triggers/content-changed-trigger.component';
export * from './pages/rules/triggers/schema-changed-trigger.component';
export * from './pages/rules/triggers/usage-trigger.component';

2
frontend/app/features/rules/module.ts

@ -16,6 +16,7 @@ import {
import {
AssetChangedTriggerComponent,
CommentTriggerComponent,
ContentChangedTriggerComponent,
GenericActionComponent,
RuleComponent,
@ -57,6 +58,7 @@ const routes: Routes = [
],
declarations: [
AssetChangedTriggerComponent,
CommentTriggerComponent,
ContentChangedTriggerComponent,
GenericActionComponent,
RuleComponent,

2
frontend/app/features/rules/pages/rules/rule-element.component.scss

@ -53,7 +53,7 @@
&-text {
font-size: .8rem;
margin-bottom: .25rem;
margin-bottom: .125rem;
margin-top: .25rem;
}

7
frontend/app/features/rules/pages/rules/rule-wizard.component.html

@ -63,6 +63,13 @@
[triggerFormSubmitted]="triggerForm.submitted | async">
</sqx-asset-changed-trigger>
</ng-container>
<ng-container *ngSwitchCase="'Comment'">
<sqx-comment-trigger
[trigger]="trigger"
[triggerForm]="triggerForm.form"
[triggerFormSubmitted]="triggerForm.submitted | async">
</sqx-comment-trigger>
</ng-container>
<ng-container *ngSwitchCase="'ContentChanged'">
<sqx-content-changed-trigger
[schemas]="schemas"

28
frontend/app/features/rules/pages/rules/triggers/comment-trigger.component.html

@ -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>

6
frontend/app/features/rules/pages/rules/triggers/comment-trigger.component.scss

@ -0,0 +1,6 @@
@import '_vars';
@import '_mixins';
textarea {
height: 100px;
}

30
frontend/app/features/rules/pages/rules/triggers/comment-trigger.component.ts

@ -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 || ''));
}
}

2
frontend/app/features/settings/pages/contributors/contributor-add-form.component.ts

@ -70,7 +70,7 @@ export class ContributorAddFormComponent implements OnChanges {
}
public ngOnChanges() {
this.defaultValue = { role: this.roles[0].name, contributorId: '' };
this.defaultValue = { role: this.roles ? this.roles[0].name : null, user: '' };
this.assignContributorForm.submitCompleted({ newValue: this.defaultValue });
}

8
frontend/app/features/settings/pages/roles/role.component.ts

@ -18,10 +18,10 @@ import {
} from '@app/shared';
const Descriptions = {
'Developer': 'Can use the API view, edit assets, contents, schemas, rules, workflows and patterns.',
'Editor': 'Can edit assets and contents and view workflows.',
'Owner': 'Can do everything, including deleting the app.',
'Reader': 'Can only read assets and contents.'
Developer: 'Can use the API view, edit assets, contents, schemas, rules, workflows and patterns.',
Editor: 'Can edit assets and contents and view workflows.',
Owner: 'Can do everything, including deleting the app.',
Reader: 'Can only read assets and contents.'
};
@Component({

6
frontend/app/framework/angular/pipes/markdown.pipe.ts

@ -11,7 +11,11 @@ import marked from 'marked';
const renderer = new marked.Renderer();
renderer.link = (href, _, text) => {
return `<a href="${href}" target="_blank", rel="noopener">${text} <i class="icon-external-link"></i></a>`;
if (href.startsWith('mailto')) {
return text;
} else {
return `<a href="${href}" target="_blank", rel="noopener">${text} <i class="icon-external-link"></i></a>`;
}
};
@Pipe({

16
frontend/app/shared/components/comment.component.html

@ -6,14 +6,6 @@
<div class="comment-message">
<div class="user-row">
<div class="user-ref">{{comment.user | sqxUserNameRef}}</div>
<button *ngIf="comment.user === userToken || canDelete" type="button" class="btn btn-sm btn-text-danger item-remove"
(sqxConfirmClick)="emitDelete()"
confirmTitle="Delete comment"
confirmText="Do you really want to delete the comment?"
[confirmRequired]="confirmDelete">
<i class="icon-bin2"></i>
</button>
</div>
<div [innerHTML]="comment.text | sqxMarkdown"></div>
@ -27,4 +19,12 @@
</div>
</div>
</div>
<button *ngIf="comment.user === userToken || canDelete" type="button" class="btn btn-sm btn-text-danger item-remove"
(sqxConfirmClick)="emitDelete()"
confirmTitle="Delete comment"
confirmText="Do you really want to delete the comment?"
[confirmRequired]="confirmDelete">
<i class="icon-bin2"></i>
</button>
</div>

2
frontend/app/shared/components/comment.component.scss

@ -30,7 +30,9 @@
font-size: .9rem;
font-weight: normal;
line-height: 1.25rem;
margin: 0;
margin-bottom: .75rem;
position: relative;
}
&-message {

4
frontend/app/shared/services/clients.service.spec.ts

@ -96,7 +96,7 @@ describe('ClientsService', () => {
const resource: Resource = {
_links: {
'update': { method: 'PUT', href: '/api/apps/my-app/clients/client1' }
update: { method: 'PUT', href: '/api/apps/my-app/clients/client1' }
}
};
@ -125,7 +125,7 @@ describe('ClientsService', () => {
const resource: Resource = {
_links: {
'delete': { method: 'DELETE', href: '/api/apps/my-app/clients/client1' }
delete: { method: 'DELETE', href: '/api/apps/my-app/clients/client1' }
}
};

2
frontend/app/shared/services/contributors.service.spec.ts

@ -93,7 +93,7 @@ describe('ContributorsService', () => {
const resource: Resource = {
_links: {
'delete': { method: 'DELETE', href: '/api/apps/my-app/contributors/123' }
delete: { method: 'DELETE', href: '/api/apps/my-app/contributors/123' }
}
};

8
frontend/app/shared/services/rules.service.spec.ts

@ -59,7 +59,7 @@ describe('RulesService', () => {
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({
'action2': {
action2: {
title: 'title2',
display: 'display2',
description: 'description2',
@ -82,7 +82,7 @@ describe('RulesService', () => {
isFormattable: true
}]
},
'action1': {
action1: {
title: 'title1',
display: 'display1',
description: 'description1',
@ -101,8 +101,8 @@ describe('RulesService', () => {
]);
expect(actions!).toEqual({
'action1': action1,
'action2': action2
action1,
action2
});
}));

18
frontend/app/shared/services/rules.service.ts

@ -34,6 +34,7 @@ export type RuleElementMetadataDto = {
export type TriggerType =
'AssetChanged' |
'Comment' |
'ContentChanged' |
'Manual' |
'SchemaChanged' |
@ -42,35 +43,42 @@ export type TriggerType =
export type TriggersDto = Record<TriggerType, RuleElementMetadataDto>;
export const ALL_TRIGGERS: TriggersDto = {
'AssetChanged': {
AssetChanged: {
description: 'For asset changes like uploaded, updated (reuploaded), renamed, deleted...',
display: 'Asset changed',
iconColor: '#3389ff',
iconCode: 'assets',
title: 'Asset changed'
},
'ContentChanged': {
Comment: {
description: 'When a user is mentioned in any comment...',
display: 'User mentioned',
iconColor: '#3389ff',
iconCode: 'comments',
title: 'User mentioned'
},
ContentChanged: {
description: 'For content changes like created, updated, published, unpublished...',
display: 'Content changed',
iconColor: '#3389ff',
iconCode: 'contents',
title: 'Content changed'
},
'Manual': {
Manual: {
description: 'To invoke processes manually, for example to update your static site...',
display: 'Manually triggered',
iconColor: '#3389ff',
iconCode: 'play-line',
title: 'Manually triggered'
},
'SchemaChanged': {
SchemaChanged: {
description: 'When a schema definition has been created, updated, published or deleted...',
display: 'Schema changed',
iconColor: '#3389ff',
iconCode: 'schemas',
title: 'Schema changed'
},
'Usage': {
Usage: {
description: 'When monthly API calls exceed a specified limit for one time a month...',
display: 'Usage exceeded',
iconColor: '#3389ff',

10
frontend/app/shared/services/schemas.service.spec.ts

@ -626,11 +626,13 @@ describe('SchemasService', () => {
version: `${id}`,
properties: {
label: `label${id}${suffix}`,
hints: `hints${id}${suffix}`,
tags: [`tags${id}${suffix}`]
tags: [
`tags${id}${suffix}`
],
hints: `hints${id}${suffix}`
},
previewUrls: {
'Default': 'url'
Default: 'url'
},
fields: [
{
@ -851,6 +853,6 @@ export function createSchemaDetails(id: number, suffix = '') {
update: '<script-update>'
},
{
'Default': 'url'
Default: 'url'
});
}

2
frontend/app/theme/_bootstrap.scss

@ -177,7 +177,7 @@ a {
}
.badge {
@include absolute(-.5rem, auto, auto, -.375rem);
@include absolute(-.25rem, auto, auto, -.375rem);
background: $color-theme-error;
font-size: .75rem;
font-weight: normal;

Loading…
Cancel
Save