Browse Source

Create content action. (#584)

pull/585/head
Sebastian Stehle 5 years ago
committed by GitHub
parent
commit
7c3a6dbb3c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 46
      backend/extensions/Squidex.Extensions/Actions/Comment/CommentActionHandler.cs
  2. 42
      backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentAction.cs
  3. 87
      backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentActionHandler.cs
  4. 21
      backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentPlugin.cs
  5. 27
      backend/extensions/Squidex.Extensions/Actions/Notification/NotificationActionHandler.cs
  6. 6
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs
  7. 6
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs
  8. 5
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs
  9. 7
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs
  10. 6
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs
  11. 2
      backend/src/Squidex.Domain.Apps.Entities/SquidexCommand.cs
  12. 2
      backend/src/Squidex.Domain.Apps.Events/SquidexEvent.cs
  13. 23
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterCompareTests.cs
  14. 18
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs
  15. 4
      frontend/app/features/rules/pages/rules/actions/generic-action.component.html

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

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.HandleRules;
@ -13,11 +12,10 @@ using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Entities.Comments.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Extensions.Actions.Comment
{
public sealed class CommentActionHandler : RuleActionHandler<CommentAction, CommentJob>
public sealed class CommentActionHandler : RuleActionHandler<CommentAction, CreateComment>
{
private const string Description = "Send a Comment";
private readonly ICommandBus commandBus;
@ -30,56 +28,48 @@ namespace Squidex.Extensions.Actions.Comment
this.commandBus = commandBus;
}
protected override async Task<(string Description, CommentJob Data)> CreateJobAsync(EnrichedEvent @event, CommentAction action)
protected override async Task<(string Description, CreateComment Data)> CreateJobAsync(EnrichedEvent @event, CommentAction action)
{
if (@event is EnrichedContentEvent contentEvent)
{
var text = await FormatAsync(action.Text, @event);
var ruleJob = new CreateComment
{
AppId = contentEvent.AppId,
};
var actor = contentEvent.Actor;
ruleJob.Text = await FormatAsync(action.Text, @event);
if (!string.IsNullOrEmpty(action.Client))
{
actor = new RefToken(RefTokenType.Client, action.Client);
ruleJob.Actor = new RefToken(RefTokenType.Client, action.Client);
}
var ruleJob = new CommentJob
else
{
AppId = contentEvent.AppId,
Actor = actor,
CommentsId = contentEvent.Id.ToString(),
Text = text
};
ruleJob.Actor = contentEvent.Actor;
}
ruleJob.CommentsId = contentEvent.Id.ToString();
return (Description, ruleJob);
}
return ("Ignore", new CommentJob());
return ("Ignore", new CreateComment());
}
protected override async Task<Result> ExecuteJobAsync(CommentJob job, CancellationToken ct = default)
protected override async Task<Result> ExecuteJobAsync(CreateComment job, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(job.CommentsId))
{
return Result.Ignored();
}
var command = SimpleMapper.Map(job, new CreateComment());
var command = job;
command.FromRule = true;
await commandBus.PublishAsync(command);
return Result.Success($"Commented: {job.Text}");
}
}
public sealed class CommentJob
{
public NamedId<Guid> AppId { get; set; }
public RefToken Actor { get; set; }
public string CommentsId { get; set; }
public string Text { get; set; }
}
}

42
backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentAction.cs

@ -0,0 +1,42 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Infrastructure.Validation;
namespace Squidex.Extensions.Actions.CreateContent
{
[RuleAction(
Title = "CreateContent",
IconImage = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 28 28'><path d='M21.875 28H6.125A6.087 6.087 0 010 21.875V6.125A6.087 6.087 0 016.125 0h15.75A6.087 6.087 0 0128 6.125v15.75A6.088 6.088 0 0121.875 28zM6.125 1.75A4.333 4.333 0 001.75 6.125v15.75a4.333 4.333 0 004.375 4.375h15.75a4.333 4.333 0 004.375-4.375V6.125a4.333 4.333 0 00-4.375-4.375H6.125z'/><path d='M13.125 12.25H7.35c-1.575 0-2.888-1.313-2.888-2.888V7.349c0-1.575 1.313-2.888 2.888-2.888h5.775c1.575 0 2.887 1.313 2.887 2.888v2.013c0 1.575-1.312 2.888-2.887 2.888zM7.35 6.212c-.613 0-1.138.525-1.138 1.138v2.012A1.16 1.16 0 007.35 10.5h5.775a1.16 1.16 0 001.138-1.138V7.349a1.16 1.16 0 00-1.138-1.138H7.35zM22.662 16.713H5.337c-.525 0-.875-.35-.875-.875s.35-.875.875-.875h17.237c.525 0 .875.35.875.875s-.35.875-.787.875zM15.138 21.262h-9.8c-.525 0-.875-.35-.875-.875s.35-.875.875-.875h9.713c.525 0 .875.35.875.875s-.35.875-.787.875z'/></svg>",
IconColor = "#3389ff",
Display = "Create content",
Description = "Create a a new content item for any schema.")]
public sealed class CreateContentAction : RuleAction
{
[LocalizedRequired]
[Display(Name = "Data", Description = "The content data.")]
[DataType(DataType.MultilineText)]
[Formattable]
public string Data { get; set; }
[LocalizedRequired]
[Display(Name = "Schema", Description = "The name of the schema.")]
[DataType(DataType.Text)]
public string Schema { get; set; }
[Display(Name = "Client", Description = "An optional client name.")]
[DataType(DataType.Text)]
public string Client { get; set; }
[Display(Name = "Publish", Description = "Publish the content.")]
[DataType(DataType.Text)]
public bool Publish { get; set; }
}
}

87
backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentActionHandler.cs

@ -0,0 +1,87 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Json;
using Command = Squidex.Domain.Apps.Entities.Contents.Commands.CreateContent;
namespace Squidex.Extensions.Actions.CreateContent
{
public sealed class CreateContentActionHandler : RuleActionHandler<CreateContentAction, Command>
{
private const string Description = "Create a content";
private readonly ICommandBus commandBus;
private readonly IAppProvider appProvider;
private readonly IJsonSerializer jsonSerializer;
public CreateContentActionHandler(RuleEventFormatter formatter, IAppProvider appProvider, ICommandBus commandBus, IJsonSerializer jsonSerializer)
: base(formatter)
{
Guard.NotNull(appProvider, nameof(appProvider));
Guard.NotNull(commandBus, nameof(commandBus));
Guard.NotNull(jsonSerializer, nameof(jsonSerializer));
this.appProvider = appProvider;
this.commandBus = commandBus;
this.jsonSerializer = jsonSerializer;
}
protected override async Task<(string Description, Command Data)> CreateJobAsync(EnrichedEvent @event, CreateContentAction action)
{
var ruleJob = new Command
{
AppId = @event.AppId,
};
var schema = await appProvider.GetSchemaAsync(@event.AppId.Id, action.Schema, true);
if (schema == null)
{
throw new InvalidOperationException($"Cannot find schema '{action.Schema}'");
}
ruleJob.SchemaId = schema.NamedId();
var json = await FormatAsync(action.Data, @event);
ruleJob.Data = jsonSerializer.Deserialize<NamedContentData>(json);
if (!string.IsNullOrEmpty(action.Client))
{
ruleJob.Actor = new RefToken(RefTokenType.Client, action.Client);
}
else if (@event is EnrichedUserEventBase userEvent)
{
ruleJob.Actor = userEvent.Actor;
}
ruleJob.Publish = action.Publish;
return (Description, ruleJob);
}
protected override async Task<Result> ExecuteJobAsync(Command job, CancellationToken ct = default)
{
var command = job;
command.FromRule = true;
await commandBus.PublishAsync(command);
return Result.Success($"Created to: {job.SchemaId.Name}");
}
}
}

21
backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentPlugin.cs

@ -0,0 +1,21 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Infrastructure.Plugins;
namespace Squidex.Extensions.Actions.CreateContent
{
public sealed class CreateContentPlugin : IPlugin
{
public void ConfigureServices(IServiceCollection services, IConfiguration config)
{
services.AddRuleAction<CreateContentAction, CreateContentActionHandler>();
}
}
}

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

@ -13,12 +13,11 @@ using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Entities.Comments.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Shared.Users;
namespace Squidex.Extensions.Actions.Notification
{
public sealed class NotificationActionHandler : RuleActionHandler<NotificationAction, NotificationJob>
public sealed class NotificationActionHandler : RuleActionHandler<NotificationAction, CreateComment>
{
private const string Description = "Send a Notification";
private static readonly NamedId<Guid> NoApp = NamedId.Of(Guid.Empty, "none");
@ -36,7 +35,7 @@ namespace Squidex.Extensions.Actions.Notification
this.userResolver = userResolver;
}
protected override async Task<(string Description, NotificationJob Data)> CreateJobAsync(EnrichedEvent @event, NotificationAction action)
protected override async Task<(string Description, CreateComment Data)> CreateJobAsync(EnrichedEvent @event, NotificationAction action)
{
if (@event is EnrichedUserEventBase userEvent)
{
@ -56,7 +55,7 @@ namespace Squidex.Extensions.Actions.Notification
throw new InvalidOperationException($"Cannot find user by '{action.User}'");
}
var ruleJob = new NotificationJob { Actor = actor, CommentsId = user.Id, Text = text };
var ruleJob = new CreateComment { Actor = actor, CommentsId = user.Id, Text = text };
if (!string.IsNullOrWhiteSpace(action.Url))
{
@ -71,32 +70,24 @@ namespace Squidex.Extensions.Actions.Notification
return (Description, ruleJob);
}
return ("Ignore", new NotificationJob());
return ("Ignore", new CreateComment());
}
protected override async Task<Result> ExecuteJobAsync(NotificationJob job, CancellationToken ct = default)
protected override async Task<Result> ExecuteJobAsync(CreateComment job, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(job.CommentsId))
{
return Result.Ignored();
}
var command = SimpleMapper.Map(job, new CreateComment { AppId = NoApp });
var command = job;
command.AppId = NoApp;
command.FromRule = true;
await commandBus.PublishAsync(command);
return Result.Success($"Notified: {job.Text}");
}
}
public sealed class NotificationJob
{
public RefToken Actor { get; set; }
public string CommentsId { get; set; }
public string Text { get; set; }
public Uri Url { get; set; }
}
}

6
backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs

@ -7,6 +7,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
@ -88,5 +89,10 @@ namespace Squidex.Domain.Apps.Core.Contents
{
return this.DictionaryHashCode();
}
public override string ToString()
{
return $"{{{string.Join(", ", this.Select(x => $"\"{x.Key}\":{x.Value.ToJsonString()}"))}}}";
}
}
}

6
backend/src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs

@ -7,6 +7,7 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Contents
@ -68,5 +69,10 @@ namespace Squidex.Domain.Apps.Core.Contents
{
return base.Equals(other);
}
public override string ToString()
{
return $"{{{string.Join(", ", this.Select(x => $"\"{x.Key}\":{x.Value}"))}}}";
}
}
}

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

@ -89,6 +89,11 @@ namespace Squidex.Domain.Apps.Core.HandleRules
var typed = @event.To<AppEvent>();
if (typed.Payload.FromRule)
{
return result;
}
var actionType = rule.Action.GetType();
if (!ruleTriggerHandlers.TryGetValue(rule.Trigger.GetType(), out var triggerHandler))

7
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using Jint;
@ -12,6 +13,7 @@ using Jint.Native;
using Jint.Native.Object;
using Jint.Runtime;
using Jint.Runtime.Descriptors;
using Orleans;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure;
@ -116,6 +118,11 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
var propertyName = property.AsString();
if (propertyName.Equals("toJSON", StringComparison.OrdinalIgnoreCase))
{
return PropertyDescriptor.Undefined;
}
return fieldProperties.GetOrAdd(propertyName, this, (k, c) => new ContentDataProperty(c, new ContentFieldObject(c, new ContentFieldData(), false)));
}

6
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using Jint;
@ -131,6 +132,11 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
var propertyName = property.AsString();
if (propertyName.Equals("toJSON", StringComparison.OrdinalIgnoreCase))
{
return PropertyDescriptor.Undefined;
}
return valueProperties?.GetOrDefault(propertyName) ?? PropertyDescriptor.Undefined;
}

2
backend/src/Squidex.Domain.Apps.Entities/SquidexCommand.cs

@ -17,6 +17,8 @@ namespace Squidex.Domain.Apps.Entities
public ClaimsPrincipal User { get; set; }
public bool FromRule { get; set; }
public long ExpectedVersion { get; set; } = EtagVersion.Auto;
}
}

2
backend/src/Squidex.Domain.Apps.Events/SquidexEvent.cs

@ -13,5 +13,7 @@ namespace Squidex.Domain.Apps.Events
public abstract class SquidexEvent : IEvent
{
public RefToken Actor { get; set; }
public bool FromRule { get; set; }
}
}

23
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterCompareTests.cs

@ -705,6 +705,29 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
Assert.Equal("[1,2,3]", result?.Replace(" ", string.Empty));
}
[Theory]
[Expressions(
"$CONTENT_DATA",
"${CONTENT_DATA}",
"${JSON.stringify(event.data)}",
null
)]
public async Task Should_return_json_string_when_data(string script)
{
var @event = new EnrichedContentEvent
{
Data =
new NamedContentData()
.AddField("city",
new ContentFieldData()
.AddJsonValue(JsonValue.Object().Add("name", "Berlin")))
};
var result = await sut.FormatAsync(script, @event);
Assert.Equal("{\"city\":{\"iv\":{\"name\":\"Berlin\"}}}", result);
}
[Theory]
[Expressions(
null,

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

@ -173,6 +173,24 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
.MustHaveHappened();
}
[Fact]
public async Task Should_not_create_job_if_event_created_by_rule()
{
var rule = ValidRule();
var @event = Envelope.Create(new ContentCreated { FromRule = true });
var jobs = await sut.CreateJobsAsync(rule, ruleId, @event);
Assert.Empty(jobs);
A.CallTo(() => ruleTriggerHandler.Trigger(@event.Payload, rule.Trigger, ruleId))
.MustNotHaveHappened();
A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventsAsync(A<Envelope<AppEvent>>._))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_not_create_job_if_not_triggered_with_precheck()
{

4
frontend/app/features/rules/pages/rules/actions/generic-action.component.html

@ -13,8 +13,8 @@
</ng-container>
<ng-container *ngSwitchCase="'Checkbox'">
<div class="form-check">
<input class="form-check-input" type="checkbox" [formControlName]="property.name" />
<label class="form-check-label" [for]="property.name">
<input class="form-check-input" type="checkbox" id="{{property.name}}" [formControlName]="property.name" />
<label class="form-check-label" for="{{property.name}}">
{{property.display}}
</label>
</div>

Loading…
Cancel
Save