Browse Source

Manual triggers (#428)

* Manual trigger for rules.

* Fixes with permission and disabled state.
pull/431/head
Sebastian Stehle 6 years ago
committed by GitHub
parent
commit
27585578fe
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs
  2. 20
      src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ManualTrigger.cs
  3. 17
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedManualEvent.cs
  4. 13
      src/Squidex.Domain.Apps.Entities/Rules/Commands/TriggerRule.cs
  5. 5
      src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs
  6. 19
      src/Squidex.Domain.Apps.Entities/Rules/IRuleEnqueuer.cs
  7. 34
      src/Squidex.Domain.Apps.Entities/Rules/ManualTriggerHandler.cs
  8. 23
      src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs
  9. 17
      src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs
  10. 13
      src/Squidex.Domain.Apps.Events/Rules/RuleManuallyTriggered.cs
  11. 5
      src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs
  12. 10
      src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs
  13. 21
      src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ManualRuleTriggerDto.cs
  14. 22
      src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs
  15. 5
      src/Squidex/Config/Domain/RuleServices.cs
  16. 4
      src/Squidex/app/features/rules/pages/rules/rule-element.component.html
  17. 10
      src/Squidex/app/features/rules/pages/rules/rule-element.component.scss
  18. 3
      src/Squidex/app/features/rules/pages/rules/rule-element.component.ts
  19. 11
      src/Squidex/app/features/rules/pages/rules/rule-wizard.component.ts
  20. 32
      src/Squidex/app/features/rules/pages/rules/rule.component.html
  21. 5
      src/Squidex/app/features/rules/pages/rules/rule.component.scss
  22. 18
      src/Squidex/app/features/rules/pages/rules/rule.component.ts
  23. 1
      src/Squidex/app/framework/angular/forms/toggle.component.scss
  24. 112
      src/Squidex/app/framework/angular/pipes/date-time.pipes.spec.ts
  25. 60
      src/Squidex/app/framework/angular/pipes/date-time.pipes.ts
  26. 19
      src/Squidex/app/shared/services/rules.service.spec.ts
  27. 40
      src/Squidex/app/shared/services/rules.service.ts
  28. 11
      src/Squidex/app/shared/state/rules.state.spec.ts
  29. 8
      src/Squidex/app/shared/state/rules.state.ts
  30. 4
      src/Squidex/app/theme/icomoon/demo-files/demo.css
  31. 670
      src/Squidex/app/theme/icomoon/demo.html
  32. BIN
      src/Squidex/app/theme/icomoon/fonts/icomoon.eot
  33. 1
      src/Squidex/app/theme/icomoon/fonts/icomoon.svg
  34. BIN
      src/Squidex/app/theme/icomoon/fonts/icomoon.ttf
  35. BIN
      src/Squidex/app/theme/icomoon/fonts/icomoon.woff
  36. 2
      src/Squidex/app/theme/icomoon/selection.json
  37. 167
      src/Squidex/app/theme/icomoon/style.css
  38. 39
      tests/Squidex.Domain.Apps.Entities.Tests/Rules/ManualTriggerHandlerTests.cs
  39. 40
      tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs
  40. 20
      tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleGrainTests.cs

2
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(ContentChangedTriggerV2 trigger);
T Visit(ManualTrigger trigger);
T Visit(SchemaChangedTrigger trigger); T Visit(SchemaChangedTrigger trigger);
T Visit(UsageTrigger trigger); T Visit(UsageTrigger trigger);

20
src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ManualTrigger.cs

@ -0,0 +1,20 @@
// ==========================================================================
// 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(ManualTrigger))]
public sealed class ManualTrigger : RuleTrigger
{
public override T Accept<T>(IRuleTriggerVisitor<T> visitor)
{
return visitor.Visit(this);
}
}
}

17
src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedManualEvent.cs

@ -0,0 +1,17 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents
{
public sealed class EnrichedManualEvent : EnrichedEvent
{
public override long Partition
{
get { return 0; }
}
}
}

13
src/Squidex.Domain.Apps.Entities/Rules/Commands/TriggerRule.cs

@ -0,0 +1,13 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Entities.Rules.Commands
{
public sealed class TriggerRule : RuleCommand
{
}
}

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

@ -41,6 +41,11 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards
return Task.FromResult(Enumerable.Empty<ValidationError>()); return Task.FromResult(Enumerable.Empty<ValidationError>());
} }
public Task<IEnumerable<ValidationError>> Visit(ManualTrigger trigger)
{
return Task.FromResult(Enumerable.Empty<ValidationError>());
}
public Task<IEnumerable<ValidationError>> Visit(SchemaChangedTrigger trigger) public Task<IEnumerable<ValidationError>> Visit(SchemaChangedTrigger trigger)
{ {
return Task.FromResult(Enumerable.Empty<ValidationError>()); return Task.FromResult(Enumerable.Empty<ValidationError>());

19
src/Squidex.Domain.Apps.Entities/Rules/IRuleEnqueuer.cs

@ -0,0 +1,19 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Entities.Rules
{
public interface IRuleEnqueuer
{
Task Enqueue(Rule rule, Guid ruleId, Envelope<IEvent> @event);
}
}

34
src/Squidex.Domain.Apps.Entities/Rules/ManualTriggerHandler.cs

@ -0,0 +1,34 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Domain.Apps.Events.Rules;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Entities.Rules
{
public sealed class ManualTriggerHandler : RuleTriggerHandler<ManualTrigger, RuleManuallyTriggered, EnrichedManualEvent>
{
protected override Task<EnrichedManualEvent> CreateEnrichedEventAsync(Envelope<RuleManuallyTriggered> @event)
{
var result = new EnrichedManualEvent
{
Name = "Manual"
};
return Task.FromResult(result);
}
protected override bool Trigger(EnrichedManualEvent @event, ManualTrigger trigger)
{
return true;
}
}
}

23
src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs

@ -10,6 +10,7 @@ using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Entities.Rules.Repositories; using Squidex.Domain.Apps.Entities.Rules.Repositories;
using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -18,7 +19,7 @@ using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Entities.Rules namespace Squidex.Domain.Apps.Entities.Rules
{ {
public sealed class RuleEnqueuer : IEventConsumer public sealed class RuleEnqueuer : IEventConsumer, IRuleEnqueuer
{ {
private static readonly TimeSpan CacheDuration = TimeSpan.FromSeconds(10); private static readonly TimeSpan CacheDuration = TimeSpan.FromSeconds(10);
private readonly IRuleEventRepository ruleEventRepository; private readonly IRuleEventRepository ruleEventRepository;
@ -62,6 +63,19 @@ namespace Squidex.Domain.Apps.Entities.Rules
return TaskHelper.Done; return TaskHelper.Done;
} }
public async Task Enqueue(Rule rule, Guid ruleId, Envelope<IEvent> @event)
{
Guard.NotNull(rule, nameof(rule));
Guard.NotNull(@event, nameof(@event));
var job = await ruleService.CreateJobAsync(rule, ruleId, @event);
if (job != null)
{
await ruleEventRepository.EnqueueAsync(job, job.Created);
}
}
public async Task On(Envelope<IEvent> @event) public async Task On(Envelope<IEvent> @event)
{ {
if (@event.Payload is AppEvent appEvent) if (@event.Payload is AppEvent appEvent)
@ -70,12 +84,7 @@ namespace Squidex.Domain.Apps.Entities.Rules
foreach (var ruleEntity in rules) foreach (var ruleEntity in rules)
{ {
var job = await ruleService.CreateJobAsync(ruleEntity.RuleDef, ruleEntity.Id, @event); await Enqueue(ruleEntity.RuleDef, ruleEntity.Id, @event);
if (job != null)
{
await ruleEventRepository.EnqueueAsync(job, job.Created);
}
} }
} }
} }

17
src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs

@ -25,13 +25,17 @@ namespace Squidex.Domain.Apps.Entities.Rules
public sealed class RuleGrain : DomainObjectGrain<RuleState>, IRuleGrain public sealed class RuleGrain : DomainObjectGrain<RuleState>, IRuleGrain
{ {
private readonly IAppProvider appProvider; private readonly IAppProvider appProvider;
private readonly IRuleEnqueuer ruleEnqueuer;
public RuleGrain(IStore<Guid> store, ISemanticLog log, IAppProvider appProvider) public RuleGrain(IStore<Guid> store, ISemanticLog log, IAppProvider appProvider, IRuleEnqueuer ruleEnqueuer)
: base(store, log) : base(store, log)
{ {
Guard.NotNull(appProvider, nameof(appProvider)); Guard.NotNull(appProvider, nameof(appProvider));
Guard.NotNull(ruleEnqueuer, nameof(ruleEnqueuer));
this.appProvider = appProvider; this.appProvider = appProvider;
this.ruleEnqueuer = ruleEnqueuer;
} }
protected override Task<object> ExecuteAsync(IAggregateCommand command) protected override Task<object> ExecuteAsync(IAggregateCommand command)
@ -83,11 +87,22 @@ namespace Squidex.Domain.Apps.Entities.Rules
Delete(c); Delete(c);
}); });
case TriggerRule triggerRule:
return Trigger(triggerRule);
default: default:
throw new NotSupportedException(); throw new NotSupportedException();
} }
} }
private async Task<object> Trigger(TriggerRule command)
{
var @event = SimpleMapper.Map(command, new RuleManuallyTriggered { RuleId = Snapshot.Id, AppId = Snapshot.AppId });
await ruleEnqueuer.Enqueue(Snapshot.RuleDef, Snapshot.Id, Envelope.Create(@event));
return null;
}
public void Create(CreateRule command) public void Create(CreateRule command)
{ {
RaiseEvent(SimpleMapper.Map(command, new RuleCreated())); RaiseEvent(SimpleMapper.Map(command, new RuleCreated()));

13
src/Squidex.Domain.Apps.Events/Rules/RuleManuallyTriggered.cs

@ -0,0 +1,13 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Events.Rules
{
public sealed class RuleManuallyTriggered : RuleEvent
{
}
}

5
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()); return SimpleMapper.Map(trigger, new AssetChangedRuleTriggerDto());
} }
public RuleTriggerDto Visit(ManualTrigger trigger)
{
return SimpleMapper.Map(trigger, new ManualRuleTriggerDto());
}
public RuleTriggerDto Visit(SchemaChangedTrigger trigger) public RuleTriggerDto Visit(SchemaChangedTrigger trigger)
{ {
return SimpleMapper.Map(trigger, new SchemaChangedRuleTriggerDto()); return SimpleMapper.Map(trigger, new SchemaChangedRuleTriggerDto());

10
src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs

@ -127,11 +127,21 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models
AddPutLink("update", controller.Url<RulesController>(x => nameof(x.PutRule), values)); AddPutLink("update", controller.Url<RulesController>(x => nameof(x.PutRule), values));
} }
if (controller.HasPermission(Permissions.AppRulesRead))
{
AddPutLink("trigger", controller.Url<RulesController>(x => nameof(x.TriggerRule), values));
}
if (controller.HasPermission(Permissions.AppRulesDelete)) if (controller.HasPermission(Permissions.AppRulesDelete))
{ {
AddDeleteLink("delete", controller.Url<RulesController>(x => nameof(x.DeleteRule), values)); AddDeleteLink("delete", controller.Url<RulesController>(x => nameof(x.DeleteRule), values));
} }
if (controller.HasPermission(Permissions.AppRulesEvents))
{
AddGetLink("logs", controller.Url<RulesController>(x => nameof(x.GetEvents), values));
}
return this; return this;
} }
} }

21
src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ManualRuleTriggerDto.cs

@ -0,0 +1,21 @@
// ==========================================================================
// 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 sealed class ManualRuleTriggerDto : RuleTriggerDto
{
public override RuleTrigger ToTrigger()
{
return SimpleMapper.Map(this, new ManualTrigger());
}
}
}

22
src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs

@ -197,6 +197,28 @@ namespace Squidex.Areas.Api.Controllers.Rules
return Ok(response); return Ok(response);
} }
/// <summary>
/// Trigger a rule.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="id">The id of the rule to disable.</param>
/// <returns>
/// 204 => Rule triggered.
/// 404 => Rule or app not found.
/// </returns>
[HttpPut]
[Route("apps/{app}/rules/{id}/trigger/")]
[ApiPermission(Permissions.AppRulesEvents)]
[ApiCosts(1)]
public async Task<IActionResult> TriggerRule(string app, Guid id)
{
var command = new TriggerRule { RuleId = id };
await CommandBus.PublishAsync(command);
return NoContent();
}
/// <summary> /// <summary>
/// Delete a rule. /// Delete a rule.
/// </summary> /// </summary>

5
src/Squidex/Config/Domain/RuleServices.cs

@ -30,6 +30,9 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<ContentChangedTriggerHandler>() services.AddSingletonAs<ContentChangedTriggerHandler>()
.As<IRuleTriggerHandler>(); .As<IRuleTriggerHandler>();
services.AddSingletonAs<ManualTriggerHandler>()
.As<IRuleTriggerHandler>();
services.AddSingletonAs<SchemaChangedTriggerHandler>() services.AddSingletonAs<SchemaChangedTriggerHandler>()
.As<IRuleTriggerHandler>(); .As<IRuleTriggerHandler>();
@ -37,7 +40,7 @@ namespace Squidex.Config.Domain
.As<IRuleTriggerHandler>(); .As<IRuleTriggerHandler>();
services.AddSingletonAs<RuleEnqueuer>() services.AddSingletonAs<RuleEnqueuer>()
.As<IEventConsumer>(); .As<IRuleEnqueuer>().As<IEventConsumer>();
services.AddSingletonAs<RuleRegistry>() services.AddSingletonAs<RuleRegistry>()
.As<ITypeProvider>().AsSelf(); .As<ITypeProvider>().AsSelf();

4
src/Squidex/app/features/rules/pages/rules/rule-element.component.html

@ -1,5 +1,5 @@
<ng-container *ngIf="isSmall; else large"> <ng-container *ngIf="isSmall; else large">
<div class="small" *ngIf="element" [style.background]="element.iconColor" [sqxHoverBackground]="element.iconColor | sqxDarken:5"> <div class="small" [class.editable]="!disabled" *ngIf="element" [style.background]="element.iconColor" [sqxHoverBackground]="element.iconColor | sqxDarken:5">
<div class="small-icon" [style.background]="element.iconColor | sqxDarken:5"> <div class="small-icon" [style.background]="element.iconColor | sqxDarken:5">
<sqx-rule-icon size="md" [element]="element"></sqx-rule-icon> <sqx-rule-icon size="md" [element]="element"></sqx-rule-icon>
</div> </div>
@ -10,7 +10,7 @@
</ng-container> </ng-container>
<ng-template #large> <ng-template #large>
<div class="row no-gutters large"> <div class="row no-gutters large" [class.editable]="!disabled">
<div class="col-auto"> <div class="col-auto">
<div class="large-icon" [style.background]="element.iconColor | sqxDarken:5"> <div class="large-icon" [style.background]="element.iconColor | sqxDarken:5">
<sqx-rule-icon size="lg" [element]="element"></sqx-rule-icon> <sqx-rule-icon size="lg" [element]="element"></sqx-rule-icon>

10
src/Squidex/app/features/rules/pages/rules/rule-element.component.scss

@ -4,11 +4,14 @@
.small { .small {
& { & {
@include transition(background-color .4s ease); @include transition(background-color .4s ease);
cursor: pointer;
height: 3rem; height: 3rem;
position: relative; position: relative;
} }
&.editable {
cursor: pointer;
}
&-text { &-text {
@include absolute(0, 0, 0, 3rem); @include absolute(0, 0, 0, 3rem);
@include truncate; @include truncate;
@ -31,11 +34,14 @@
.large { .large {
& { & {
cursor: pointer;
min-height: 100px; min-height: 100px;
max-height: 100px; max-height: 100px;
} }
&.editable {
cursor: pointer;
}
&-link { &-link {
font-size: .8rem; font-size: .8rem;
} }

3
src/Squidex/app/features/rules/pages/rules/rule-element.component.ts

@ -24,4 +24,7 @@ export class RuleElementComponent {
@Input() @Input()
public isSmall = true; public isSmall = true;
@Input()
public disabled = false;
} }

11
src/Squidex/app/features/rules/pages/rules/rule-wizard.component.ts

@ -13,7 +13,8 @@ import {
RuleDto, RuleDto,
RuleElementDto, RuleElementDto,
RulesState, RulesState,
SchemaDto SchemaDto,
TriggerType
} from '@app/shared'; } from '@app/shared';
const MODE_WIZARD = 'Wizard'; const MODE_WIZARD = 'Wizard';
@ -104,10 +105,16 @@ export class RuleWizardComponent implements AfterViewInit, OnInit {
this.complete.emit(); this.complete.emit();
} }
public selectTriggerType(type: string) { public selectTriggerType(type: TriggerType) {
this.triggerType = type; this.triggerType = type;
if (type === 'Manual') {
this.trigger = { triggerType: type };
this.step += 2;
} else {
this.step++; this.step++;
} }
}
public selectActionType(type: string) { public selectActionType(type: string) {
this.actionType = type; this.actionType = type;

32
src/Squidex/app/features/rules/pages/rules/rule.component.html

@ -28,20 +28,32 @@
<h3>If</h3> <h3>If</h3>
</div> </div>
<div class="col"> <div class="col">
<span (click)="editTrigger.emit()"> <span (click)="emitEditTrigger()">
<sqx-rule-element [type]="rule.triggerType" [element]="ruleTriggers[rule.triggerType]"></sqx-rule-element> <sqx-rule-element [type]="rule.triggerType" [element]="ruleTriggers[rule.triggerType]" [disabled]="isManual"></sqx-rule-element>
</span> </span>
</div> </div>
<div class="col col-auto"> <div class="col col-auto">
<h3>then</h3> <h3>then</h3>
</div> </div>
<div class="col"> <div class="col">
<span (click)="editAction.emit()"> <span (click)="emitEditAction()">
<sqx-rule-element [type]="rule.actionType" [element]="ruleActions[rule.actionType]"></sqx-rule-element> <sqx-rule-element [type]="rule.actionType" [element]="ruleActions[rule.actionType]"></sqx-rule-element>
</span> </span>
</div> </div>
<div class="col col-auto"> <div class="col col-last text-right">
<ng-container *ngIf="isManual; else notManual">
<button class="btn btn-secondary"
[disabled]="!rule.canTrigger"
(sqxConfirmClick)="trigger()"
confirmTitle="Trigger rule"
confirmText="Do you really want to trigger the rule?">
<i class="icon-play-line"></i>
</button>
</ng-container>
<ng-template #notManual>
<sqx-toggle [disabled]="!rule.canDisable && !rule.canEnable" [ngModel]="rule.isEnabled" (ngModelChange)="toggle()"></sqx-toggle> <sqx-toggle [disabled]="!rule.canDisable && !rule.canEnable" [ngModel]="rule.isEnabled" (ngModelChange)="toggle()"></sqx-toggle>
</ng-template>
</div> </div>
</div> </div>
</div> </div>
@ -54,18 +66,10 @@
Failed: <strong>{{rule.numFailed}}</strong> Failed: <strong>{{rule.numFailed}}</strong>
</div> </div>
<div class="col"> <div class="col">
Last Executed: Executed: <span>{{rule.lastExecuted | sqxFromNow:'-'}}</span>
<ng-container *ngIf="rule.lastExecuted; else notExecuted">
{{rule.lastExecuted | sqxFromNow}}
</ng-container>
<ng-template #notExecuted>
-
</ng-template>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<a routerLink="events" [queryParams]="{ ruleId: rule.id }"> <a routerLink="events" [queryParams]="{ ruleId: rule.id }" *ngIf="rule.canTrigger">
Logs Logs
</a> </a>
</div> </div>

5
src/Squidex/app/features/rules/pages/rules/rule.component.scss

@ -20,3 +20,8 @@
font-size: 90%; font-size: 90%;
} }
} }
.col-last {
min-width: 80px;
max-width: 80px;
}

18
src/Squidex/app/features/rules/pages/rules/rule.component.ts

@ -38,6 +38,10 @@ export class RuleComponent {
@Input() @Input()
public rule: RuleDto; public rule: RuleDto;
public get isManual() {
return this.rule.triggerType === 'Manual';
}
constructor( constructor(
private readonly rulesState: RulesState private readonly rulesState: RulesState
) { ) {
@ -51,6 +55,20 @@ export class RuleComponent {
this.rulesState.rename(this.rule, name); this.rulesState.rename(this.rule, name);
} }
public trigger() {
this.rulesState.trigger(this.rule);
}
public emitEditAction() {
this.editAction.emit();
}
public emitEditTrigger() {
if (!this.isManual) {
this.editTrigger.emit();
}
}
public toggle() { public toggle() {
if (this.rule.isEnabled) { if (this.rule.isEnabled) {
this.rulesState.disable(this.rule); this.rulesState.disable(this.rule);

1
src/Squidex/app/framework/angular/forms/toggle.component.scss

@ -22,6 +22,7 @@ $toggle-button-size: $toggle-height - .25rem;
@include border-radius($toggle-height * .5); @include border-radius($toggle-height * .5);
@include box-shadow-inner; @include box-shadow-inner;
@include transition(background-color .3s ease); @include transition(background-color .3s ease);
display: inline-block;
position: relative; position: relative;
background: lighten($color-border, 6%); background: lighten($color-border, 6%);
border: 0; border: 0;

112
src/Squidex/app/framework/angular/pipes/date-time.pipes.spec.ts

@ -33,17 +33,33 @@ describe('DurationPipe', () => {
expect(actual).toBe(expected); expect(actual).toBe(expected);
}); });
[null, undefined].map(x => {
it('should use fallback for non value', () => {
const actual = new DurationPipe().transform(x, '-');
expect(actual).toBe('-');
});
});
}); });
describe('FullDateTimePipe', () => { describe('DatePipe', () => {
it('should format to nice string', () => { it('should format to two digit day number and short month name and year', () => {
const pipe = new FullDateTimePipe(); const pipe = new DatePipe();
const actual = pipe.transform(dateTime); const actual = pipe.transform(dateTime);
const expected = 'Thursday, October 3, 2013 12:13 PM'; const expected = '03. Oct 2013';
expect(actual).toBe(expected); expect(actual).toBe(expected);
}); });
[null, undefined].map(x => {
it('should use fallback for non value', () => {
const actual = new DatePipe().transform(x, '-');
expect(actual).toBe('-');
});
});
}); });
describe('DayPipe', () => { describe('DayPipe', () => {
@ -55,17 +71,33 @@ describe('DayPipe', () => {
expect(actual).toBe(expected); expect(actual).toBe(expected);
}); });
[null, undefined].map(x => {
it('should use fallback for non value', () => {
const actual = new DayPipe().transform(x, '-');
expect(actual).toBe('-');
});
});
}); });
describe('MonthPipe', () => { describe('DayOfWeekPipe', () => {
it('should format to long month name', () => { it('should format to short week of day string', () => {
const pipe = new MonthPipe(); const pipe = new DayOfWeekPipe();
const actual = pipe.transform(dateTime); const actual = pipe.transform(dateTime);
const expected = 'October'; const expected = 'Th';
expect(actual).toBe(expected); expect(actual).toBe(expected);
}); });
[null, undefined].map(x => {
it('should use fallback for non value', () => {
const actual = new DayOfWeekPipe().transform(x, '-');
expect(actual).toBe('-');
});
});
}); });
describe('FromNowPipe', () => { describe('FromNowPipe', () => {
@ -77,28 +109,52 @@ describe('FromNowPipe', () => {
expect(actual).toBe(expected); expect(actual).toBe(expected);
}); });
[null, undefined].map(x => {
it('should use fallback for non value', () => {
const actual = new FromNowPipe().transform(x, '-');
expect(actual).toBe('-');
});
});
}); });
describe('DayOfWeekPipe', () => { describe('FullDateTimePipe', () => {
it('should format to short week of day string', () => { it('should format to nice string', () => {
const pipe = new DayOfWeekPipe(); const pipe = new FullDateTimePipe();
const actual = pipe.transform(dateTime); const actual = pipe.transform(dateTime);
const expected = 'Th'; const expected = 'Thursday, October 3, 2013 12:13 PM';
expect(actual).toBe(expected); expect(actual).toBe(expected);
}); });
[null, undefined].map(x => {
it('should use fallback for non value', () => {
const actual = new FullDateTimePipe().transform(x, '-');
expect(actual).toBe('-');
});
});
}); });
describe('DatePipe', () => { describe('MonthPipe', () => {
it('should format to two digit day number and short month name and year', () => { it('should format to long month name', () => {
const pipe = new DatePipe(); const pipe = new MonthPipe();
const actual = pipe.transform(dateTime); const actual = pipe.transform(dateTime);
const expected = '03. Oct 2013'; const expected = 'October';
expect(actual).toBe(expected); expect(actual).toBe(expected);
}); });
[null, undefined].map(x => {
it('should use fallback for non value', () => {
const actual = new MonthPipe().transform(x, '-');
expect(actual).toBe('-');
});
});
}); });
describe('ShortDatePipe', () => { describe('ShortDatePipe', () => {
@ -110,6 +166,14 @@ describe('ShortDatePipe', () => {
expect(actual).toBe(expected); expect(actual).toBe(expected);
}); });
[null, undefined].map(x => {
it('should use fallback for non value', () => {
const actual = new ShortDatePipe().transform(x, '-');
expect(actual).toBe('-');
});
});
}); });
describe('ShortTimePipe', () => { describe('ShortTimePipe', () => {
@ -121,6 +185,14 @@ describe('ShortTimePipe', () => {
expect(actual).toBe(expected); expect(actual).toBe(expected);
}); });
[null, undefined].map(x => {
it('should use fallback for non value', () => {
const actual = new ShortTimePipe().transform(x, '-');
expect(actual).toBe('-');
});
});
}); });
describe('ISODatePipe', () => { describe('ISODatePipe', () => {
@ -132,4 +204,12 @@ describe('ISODatePipe', () => {
expect(actual).toBe(expected); expect(actual).toBe(expected);
}); });
[null, undefined].map(x => {
it('should use fallback for non value', () => {
const actual = new ISODatePipe().transform(x, '-');
expect(actual).toBe('-');
});
});
}); });

60
src/Squidex/app/framework/angular/pipes/date-time.pipes.ts

@ -14,7 +14,11 @@ import { DateTime, Duration } from '@app/framework/internal';
pure: true pure: true
}) })
export class ShortDatePipe implements PipeTransform { export class ShortDatePipe implements PipeTransform {
public transform(value: DateTime): any { public transform(value: DateTime | undefined | null, fallback = ''): string {
if (!value) {
return fallback;
}
return value.toStringFormat('DD. MMM'); return value.toStringFormat('DD. MMM');
} }
} }
@ -24,7 +28,11 @@ export class ShortDatePipe implements PipeTransform {
pure: true pure: true
}) })
export class ISODatePipe implements PipeTransform { export class ISODatePipe implements PipeTransform {
public transform(value: DateTime): any { public transform(value: DateTime | undefined | null, fallback = ''): string {
if (!value) {
return fallback;
}
return value.toISOString(); return value.toISOString();
} }
} }
@ -34,7 +42,11 @@ export class ISODatePipe implements PipeTransform {
pure: true pure: true
}) })
export class DatePipe implements PipeTransform { export class DatePipe implements PipeTransform {
public transform(value: DateTime): any { public transform(value: DateTime | undefined | null, fallback = ''): string {
if (!value) {
return fallback;
}
return value.toStringFormat('DD. MMM YYYY'); return value.toStringFormat('DD. MMM YYYY');
} }
} }
@ -44,7 +56,11 @@ export class DatePipe implements PipeTransform {
pure: true pure: true
}) })
export class MonthPipe implements PipeTransform { export class MonthPipe implements PipeTransform {
public transform(value: DateTime): any { public transform(value: DateTime | undefined | null, fallback = ''): string {
if (!value) {
return fallback;
}
return value.toStringFormat('MMMM'); return value.toStringFormat('MMMM');
} }
} }
@ -54,7 +70,11 @@ export class MonthPipe implements PipeTransform {
pure: true pure: true
}) })
export class FromNowPipe implements PipeTransform { export class FromNowPipe implements PipeTransform {
public transform(value: DateTime): any { public transform(value: DateTime | undefined | null, fallback = ''): string {
if (!value) {
return fallback;
}
return value.toFromNow(); return value.toFromNow();
} }
} }
@ -64,7 +84,11 @@ export class FromNowPipe implements PipeTransform {
pure: true pure: true
}) })
export class DayOfWeekPipe implements PipeTransform { export class DayOfWeekPipe implements PipeTransform {
public transform(value: DateTime): any { public transform(value: DateTime | undefined | null, fallback = ''): string {
if (!value) {
return fallback;
}
return value.toStringFormat('dd'); return value.toStringFormat('dd');
} }
} }
@ -74,7 +98,11 @@ export class DayOfWeekPipe implements PipeTransform {
pure: true pure: true
}) })
export class DayPipe implements PipeTransform { export class DayPipe implements PipeTransform {
public transform(value: DateTime): any { public transform(value: DateTime | undefined | null, fallback = ''): string {
if (!value) {
return fallback;
}
return value.toStringFormat('DD'); return value.toStringFormat('DD');
} }
} }
@ -84,7 +112,11 @@ export class DayPipe implements PipeTransform {
pure: true pure: true
}) })
export class ShortTimePipe implements PipeTransform { export class ShortTimePipe implements PipeTransform {
public transform(value: DateTime): any { public transform(value: DateTime | undefined | null, fallback = ''): string {
if (!value) {
return fallback;
}
return value.toStringFormat('HH:mm'); return value.toStringFormat('HH:mm');
} }
} }
@ -94,7 +126,11 @@ export class ShortTimePipe implements PipeTransform {
pure: true pure: true
}) })
export class FullDateTimePipe implements PipeTransform { export class FullDateTimePipe implements PipeTransform {
public transform(value: DateTime): any { public transform(value: DateTime | undefined | null, fallback = ''): string {
if (!value) {
return fallback;
}
return value.toStringFormat('LLLL'); return value.toStringFormat('LLLL');
} }
} }
@ -104,7 +140,11 @@ export class FullDateTimePipe implements PipeTransform {
pure: true pure: true
}) })
export class DurationPipe implements PipeTransform { export class DurationPipe implements PipeTransform {
public transform(value: Duration): any { public transform(value: Duration | undefined | null, fallback = ''): string {
if (!value) {
return fallback;
}
return value.toString(); return value.toString();
} }
} }

19
src/Squidex/app/shared/services/rules.service.spec.ts

@ -269,6 +269,25 @@ describe('RulesService', () => {
req.flush({}); req.flush({});
})); }));
it('should make put request to trigger rule',
inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => {
const resource: Resource = {
_links: {
trigger: { method: 'PUT', href: '/api/apps/my-app/rules/123/trigger' }
}
};
rulesService.triggerRule('my-app', resource).subscribe();
const req = httpMock.expectOne('http://service/p/api/apps/my-app/rules/123/trigger');
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({});
}));
it('should make get request to get app rule events', it('should make get request to get app rule events',
inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => {

40
src/Squidex/app/shared/services/rules.service.ts

@ -32,9 +32,23 @@ export type RuleElementMetadataDto = {
title?: string; title?: string;
}; };
export type TriggersDto = { [key: string]: RuleElementMetadataDto }; export type TriggerType =
'AssetChanged' |
'ContentChanged' |
'Manual' |
'SchemaChanged' |
'Usage';
export type TriggersDto = Record<TriggerType, RuleElementMetadataDto>;
export const ALL_TRIGGERS: TriggersDto = { export const ALL_TRIGGERS: TriggersDto = {
'AssetChanged': {
description: 'For asset changes like uploaded, updated (reuploaded), renamed, deleted...',
display: 'Asset changed',
iconColor: '#3389ff',
iconCode: 'assets',
title: 'Asset changed'
},
'ContentChanged': { 'ContentChanged': {
description: 'For content changes like created, updated, published, unpublished...', description: 'For content changes like created, updated, published, unpublished...',
display: 'Content changed', display: 'Content changed',
@ -42,12 +56,12 @@ export const ALL_TRIGGERS: TriggersDto = {
iconCode: 'contents', iconCode: 'contents',
title: 'Content changed' title: 'Content changed'
}, },
'AssetChanged': { 'Manual': {
description: 'For asset changes like uploaded, updated (reuploaded), renamed, deleted...', description: 'To invoke processes manually, for example to update your static site...',
display: 'Asset changed', display: 'Manually triggered',
iconColor: '#3389ff', iconColor: '#3389ff',
iconCode: 'assets', iconCode: 'play-line',
title: 'Asset changed' title: 'Manually triggered'
}, },
'SchemaChanged': { 'SchemaChanged': {
description: 'When a schema definition has been created, updated, published or deleted...', description: 'When a schema definition has been created, updated, published or deleted...',
@ -113,6 +127,7 @@ export class RuleDto {
public readonly canDelete: boolean; public readonly canDelete: boolean;
public readonly canDisable: boolean; public readonly canDisable: boolean;
public readonly canEnable: boolean; public readonly canEnable: boolean;
public readonly canTrigger: boolean;
public readonly canUpdate: boolean; public readonly canUpdate: boolean;
constructor( constructor(
@ -138,6 +153,7 @@ export class RuleDto {
this.canDelete = hasAnyLink(links, 'delete'); this.canDelete = hasAnyLink(links, 'delete');
this.canDisable = hasAnyLink(links, 'disable'); this.canDisable = hasAnyLink(links, 'disable');
this.canEnable = hasAnyLink(links, 'enable'); this.canEnable = hasAnyLink(links, 'enable');
this.canTrigger = hasAnyLink(links, 'logs');
this.canUpdate = hasAnyLink(links, 'update'); this.canUpdate = hasAnyLink(links, 'update');
} }
} }
@ -309,6 +325,18 @@ export class RulesService {
pretifyError('Failed to delete rule. Please reload.')); pretifyError('Failed to delete rule. Please reload.'));
} }
public triggerRule(appName: string, resource: Resource): Observable<any> {
const link = resource._links['trigger'];
const url = this.apiUrl.buildUrl(link.href);
return this.http.request(link.method, url, {}).pipe(
tap(() => {
this.analytics.trackEvent('Rule', 'Triggered', appName);
}),
pretifyError('Failed to trigger rule. Please reload.'));
}
public getEvents(appName: string, take: number, skip: number, ruleId?: string): Observable<RuleEventsDto> { public getEvents(appName: string, take: number, skip: number, ruleId?: string): Observable<RuleEventsDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/events?take=${take}&skip=${skip}&ruleId=${ruleId || ''}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/events?take=${take}&skip=${skip}&ruleId=${ruleId || ''}`);

11
src/Squidex/app/shared/state/rules.state.spec.ts

@ -152,6 +152,17 @@ describe('RulesState', () => {
expect(rule1New).toEqual(updated); expect(rule1New).toEqual(updated);
}); });
it('should not update rule when triggered', () => {
rulesService.setup(x => x.triggerRule(app, rule1))
.returns(() => of()).verifiable();
rulesState.trigger(rule1).subscribe();
const rule1New = rulesState.snapshot.rules[0];
expect(rule1New).toEqual(rule1);
});
it('should update rule when disabled', () => { it('should update rule when disabled', () => {
const updated = createRule(1, '_new'); const updated = createRule(1, '_new');

8
src/Squidex/app/shared/state/rules.state.ts

@ -148,6 +148,14 @@ export class RulesState extends State<Snapshot> {
shareSubscribed(this.dialogs)); shareSubscribed(this.dialogs));
} }
public trigger(rule: RuleDto): Observable<any> {
return this.rulesService.triggerRule(this.appName, rule).pipe(
tap(() => {
this.dialogs.notifyInfo('Rule has been added to the queue.');
}),
shareSubscribed(this.dialogs));
}
private replaceRule(rule: RuleDto) { private replaceRule(rule: RuleDto) {
this.next(s => { this.next(s => {
const rules = s.rules.replaceBy('id', rule); const rules = s.rules.replaceBy('id', rule);

4
src/Squidex/app/theme/icomoon/demo-files/demo.css

@ -147,10 +147,10 @@ p {
font-size: 16px; font-size: 16px;
} }
.fs1 { .fs1 {
font-size: 28px; font-size: 24px;
} }
.fs2 { .fs2 {
font-size: 24px; font-size: 28px;
} }
.fs3 { .fs3 {
font-size: 24px; font-size: 24px;

670
src/Squidex/app/theme/icomoon/demo.html

File diff suppressed because it is too large

BIN
src/Squidex/app/theme/icomoon/fonts/icomoon.eot

Binary file not shown.

1
src/Squidex/app/theme/icomoon/fonts/icomoon.svg

@ -128,6 +128,7 @@
<glyph unicode="&#xe976;" glyph-name="arrow-right" d="M621.254 82.746l320 320c24.994 24.992 24.994 65.516 0 90.51l-320 320c-24.994 24.992-65.516 24.992-90.51 0-24.994-24.994-24.994-65.516 0-90.51l210.746-210.746h-613.49c-35.346 0-64-28.654-64-64s28.654-64 64-64h613.49l-210.746-210.746c-12.496-12.496-18.744-28.876-18.744-45.254s6.248-32.758 18.744-45.254c24.994-24.994 65.516-24.994 90.51 0z" /> <glyph unicode="&#xe976;" glyph-name="arrow-right" d="M621.254 82.746l320 320c24.994 24.992 24.994 65.516 0 90.51l-320 320c-24.994 24.992-65.516 24.992-90.51 0-24.994-24.994-24.994-65.516 0-90.51l210.746-210.746h-613.49c-35.346 0-64-28.654-64-64s28.654-64 64-64h613.49l-210.746-210.746c-12.496-12.496-18.744-28.876-18.744-45.254s6.248-32.758 18.744-45.254c24.994-24.994 65.516-24.994 90.51 0z" />
<glyph unicode="&#xe977;" glyph-name="corner-down-right" d="M128 768v-298.667c0-58.88 23.936-112.299 62.464-150.869s91.989-62.464 150.869-62.464h409.003l-140.501-140.501c-16.683-16.683-16.683-43.691 0-60.331s43.691-16.683 60.331 0l213.333 213.333c3.925 3.925 7.083 8.619 9.259 13.824s3.243 10.795 3.243 16.341c0 10.923-4.181 21.845-12.501 30.165l-213.333 213.333c-16.683 16.683-43.691 16.683-60.331 0s-16.683-43.691 0-60.331l140.501-140.501h-409.003c-35.371 0-67.285 14.293-90.496 37.504s-37.504 55.125-37.504 90.496v298.667c0 23.552-19.115 42.667-42.667 42.667s-42.667-19.115-42.667-42.667z" /> <glyph unicode="&#xe977;" glyph-name="corner-down-right" d="M128 768v-298.667c0-58.88 23.936-112.299 62.464-150.869s91.989-62.464 150.869-62.464h409.003l-140.501-140.501c-16.683-16.683-16.683-43.691 0-60.331s43.691-16.683 60.331 0l213.333 213.333c3.925 3.925 7.083 8.619 9.259 13.824s3.243 10.795 3.243 16.341c0 10.923-4.181 21.845-12.501 30.165l-213.333 213.333c-16.683 16.683-43.691 16.683-60.331 0s-16.683-43.691 0-60.331l140.501-140.501h-409.003c-35.371 0-67.285 14.293-90.496 37.504s-37.504 55.125-37.504 90.496v298.667c0 23.552-19.115 42.667-42.667 42.667s-42.667-19.115-42.667-42.667z" />
<glyph unicode="&#xe978;" glyph-name="filter-filled" horiz-adv-x="805" d="M801.714 782.286c5.714-13.714 2.857-29.714-8-40l-281.714-281.714v-424c0-14.857-9.143-28-22.286-33.714-4.571-1.714-9.714-2.857-14.286-2.857-9.714 0-18.857 3.429-25.714 10.857l-146.286 146.286c-6.857 6.857-10.857 16-10.857 25.714v277.714l-281.714 281.714c-10.857 10.286-13.714 26.286-8 40 5.714 13.143 18.857 22.286 33.714 22.286h731.429c14.857 0 28-9.143 33.714-22.286z" /> <glyph unicode="&#xe978;" glyph-name="filter-filled" horiz-adv-x="805" d="M801.714 782.286c5.714-13.714 2.857-29.714-8-40l-281.714-281.714v-424c0-14.857-9.143-28-22.286-33.714-4.571-1.714-9.714-2.857-14.286-2.857-9.714 0-18.857 3.429-25.714 10.857l-146.286 146.286c-6.857 6.857-10.857 16-10.857 25.714v277.714l-281.714 281.714c-10.857 10.286-13.714 26.286-8 40 5.714 13.143 18.857 22.286 33.714 22.286h731.429c14.857 0 28-9.143 33.714-22.286z" />
<glyph unicode="&#xe979;" glyph-name="trigger-Manual, play-line" d="M236.416 846.55c-6.528 4.267-14.507 6.784-23.083 6.784-23.552 0-42.667-19.115-42.667-42.667v-768c-0.043-7.765 2.133-15.872 6.784-23.083 12.757-19.84 39.125-25.557 58.965-12.8l597.333 384c4.864 3.072 9.344 7.424 12.8 12.8 12.757 19.84 6.997 46.208-12.8 58.965zM256 732.502l475.776-305.835-475.776-305.835z" />
<glyph unicode="&#xe9ca;" glyph-name="earth" d="M512 960c-282.77 0-512-229.23-512-512s229.23-512 512-512 512 229.23 512 512-229.23 512-512 512zM512-0.002c-62.958 0-122.872 13.012-177.23 36.452l233.148 262.29c5.206 5.858 8.082 13.422 8.082 21.26v96c0 17.674-14.326 32-32 32-112.99 0-232.204 117.462-233.374 118.626-6 6.002-14.14 9.374-22.626 9.374h-128c-17.672 0-32-14.328-32-32v-192c0-12.122 6.848-23.202 17.69-28.622l110.31-55.156v-187.886c-116.052 80.956-192 215.432-192 367.664 0 68.714 15.49 133.806 43.138 192h116.862c8.488 0 16.626 3.372 22.628 9.372l128 128c6 6.002 9.372 14.14 9.372 22.628v77.412c40.562 12.074 83.518 18.588 128 18.588 70.406 0 137.004-16.26 196.282-45.2-4.144-3.502-8.176-7.164-12.046-11.036-36.266-36.264-56.236-84.478-56.236-135.764s19.97-99.5 56.236-135.764c36.434-36.432 85.218-56.264 135.634-56.26 3.166 0 6.342 0.080 9.518 0.236 13.814-51.802 38.752-186.656-8.404-372.334-0.444-1.744-0.696-3.488-0.842-5.224-81.324-83.080-194.7-134.656-320.142-134.656z" /> <glyph unicode="&#xe9ca;" glyph-name="earth" d="M512 960c-282.77 0-512-229.23-512-512s229.23-512 512-512 512 229.23 512 512-229.23 512-512 512zM512-0.002c-62.958 0-122.872 13.012-177.23 36.452l233.148 262.29c5.206 5.858 8.082 13.422 8.082 21.26v96c0 17.674-14.326 32-32 32-112.99 0-232.204 117.462-233.374 118.626-6 6.002-14.14 9.374-22.626 9.374h-128c-17.672 0-32-14.328-32-32v-192c0-12.122 6.848-23.202 17.69-28.622l110.31-55.156v-187.886c-116.052 80.956-192 215.432-192 367.664 0 68.714 15.49 133.806 43.138 192h116.862c8.488 0 16.626 3.372 22.628 9.372l128 128c6 6.002 9.372 14.14 9.372 22.628v77.412c40.562 12.074 83.518 18.588 128 18.588 70.406 0 137.004-16.26 196.282-45.2-4.144-3.502-8.176-7.164-12.046-11.036-36.266-36.264-56.236-84.478-56.236-135.764s19.97-99.5 56.236-135.764c36.434-36.432 85.218-56.264 135.634-56.26 3.166 0 6.342 0.080 9.518 0.236 13.814-51.802 38.752-186.656-8.404-372.334-0.444-1.744-0.696-3.488-0.842-5.224-81.324-83.080-194.7-134.656-320.142-134.656z" />
<glyph unicode="&#xf00a;" glyph-name="grid" d="M292.571 237.714v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM292.571 530.286v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM658.286 237.714v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM292.571 822.857v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM658.286 530.286v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM1024 237.714v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM658.286 822.857v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM1024 530.286v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM1024 822.857v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857z" /> <glyph unicode="&#xf00a;" glyph-name="grid" d="M292.571 237.714v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM292.571 530.286v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM658.286 237.714v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM292.571 822.857v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM658.286 530.286v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM1024 237.714v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM658.286 822.857v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM1024 530.286v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM1024 822.857v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857z" />
<glyph unicode="&#xf0c9;" glyph-name="list1" horiz-adv-x="878" d="M877.714 182.857v-73.143c0-20-16.571-36.571-36.571-36.571h-804.571c-20 0-36.571 16.571-36.571 36.571v73.143c0 20 16.571 36.571 36.571 36.571h804.571c20 0 36.571-16.571 36.571-36.571zM877.714 475.428v-73.143c0-20-16.571-36.571-36.571-36.571h-804.571c-20 0-36.571 16.571-36.571 36.571v73.143c0 20 16.571 36.571 36.571 36.571h804.571c20 0 36.571-16.571 36.571-36.571zM877.714 768v-73.143c0-20-16.571-36.571-36.571-36.571h-804.571c-20 0-36.571 16.571-36.571 36.571v73.143c0 20 16.571 36.571 36.571 36.571h804.571c20 0 36.571-16.571 36.571-36.571z" /> <glyph unicode="&#xf0c9;" glyph-name="list1" horiz-adv-x="878" d="M877.714 182.857v-73.143c0-20-16.571-36.571-36.571-36.571h-804.571c-20 0-36.571 16.571-36.571 36.571v73.143c0 20 16.571 36.571 36.571 36.571h804.571c20 0 36.571-16.571 36.571-36.571zM877.714 475.428v-73.143c0-20-16.571-36.571-36.571-36.571h-804.571c-20 0-36.571 16.571-36.571 36.571v73.143c0 20 16.571 36.571 36.571 36.571h804.571c20 0 36.571-16.571 36.571-36.571zM877.714 768v-73.143c0-20-16.571-36.571-36.571-36.571h-804.571c-20 0-36.571 16.571-36.571 36.571v73.143c0 20 16.571 36.571 36.571 36.571h804.571c20 0 36.571-16.571 36.571-36.571z" />

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 96 KiB

BIN
src/Squidex/app/theme/icomoon/fonts/icomoon.ttf

Binary file not shown.

BIN
src/Squidex/app/theme/icomoon/fonts/icomoon.woff

Binary file not shown.

2
src/Squidex/app/theme/icomoon/selection.json

File diff suppressed because one or more lines are too long

167
src/Squidex/app/theme/icomoon/style.css

@ -1,12 +1,13 @@
@font-face { @font-face {
font-family: 'icomoon'; font-family: 'icomoon';
src: url('fonts/icomoon.eot?ha0h5n'); src: url('fonts/icomoon.eot?kqipf');
src: url('fonts/icomoon.eot?ha0h5n#iefix') format('embedded-opentype'), src: url('fonts/icomoon.eot?kqipf#iefix') format('embedded-opentype'),
url('fonts/icomoon.ttf?ha0h5n') format('truetype'), url('fonts/icomoon.ttf?kqipf') format('truetype'),
url('fonts/icomoon.woff?ha0h5n') format('woff'), url('fonts/icomoon.woff?kqipf') format('woff'),
url('fonts/icomoon.svg?ha0h5n#icomoon') format('svg'); url('fonts/icomoon.svg?kqipf#icomoon') format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
font-display: block;
} }
[class^="icon-"], [class*=" icon-"] { [class^="icon-"], [class*=" icon-"] {
@ -24,6 +25,54 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.icon-trigger-Manual:before {
content: "\e979";
}
.icon-play-line:before {
content: "\e979";
}
.icon-corner-down-right:before {
content: "\e977";
}
.icon-info-outline:before {
content: "\e974";
}
.icon-upload-2:before {
content: "\e972";
}
.icon-translate:before {
content: "\e96f";
}
.icon-arrow_back:before {
content: "\e96e";
}
.icon-external-link:before {
content: "\e96d";
}
.icon-minus-square:before {
content: "\e969";
}
.icon-plus-square:before {
content: "\e968";
}
.icon-drag2:before {
content: "\e961";
}
.icon-comments:before {
content: "\e95f";
}
.icon-backup:before {
content: "\e95b";
}
.icon-support:before {
content: "\e95a";
}
.icon-control-RichText:before {
content: "\e939";
}
.icon-download:before {
content: "\e93e";
}
.icon-filter-filled:before { .icon-filter-filled:before {
content: "\e978"; content: "\e978";
} }
@ -120,114 +169,6 @@
.icon-rules:before { .icon-rules:before {
content: "\e947"; content: "\e947";
} }
.icon-corner-down-right:before {
content: "\e977";
}
.icon-info-outline:before {
content: "\e974";
}
.icon-upload-2:before {
content: "\e972";
}
.icon-translate:before {
content: "\e96f";
}
.icon-arrow_back:before {
content: "\e96e";
}
.icon-external-link:before {
content: "\e96d";
}
.icon-minus-square:before {
content: "\e969";
}
.icon-plus-square:before {
content: "\e968";
}
.icon-drag2:before {
content: "\e961";
}
.icon-comments:before {
content: "\e95f";
}
.icon-backup:before {
content: "\e95b";
}
.icon-support:before {
content: "\e95a";
}
.icon-control-RichText:before {
content: "\e939";
}
.icon-download:before {
content: "\e93e";
}
.icon-control-Radio:before {
content: "\e90d";
}
.icon-control-TextArea:before {
content: "\e90e";
}
.icon-control-Toggle:before {
content: "\e90f";
}
.icon-copy:before {
content: "\e910";
}
.icon-dashboard:before {
content: "\e911";
}
.icon-delete:before {
content: "\e912";
}
.icon-bin:before {
content: "\e912";
}
.icon-delete-filled:before {
content: "\e913";
}
.icon-document-delete:before {
content: "\e914";
}
.icon-document-disable:before {
content: "\e915";
}
.icon-document-publish:before {
content: "\e916";
}
.icon-drag:before {
content: "\e917";
}
.icon-filter:before {
content: "\e918";
}
.icon-github:before {
content: "\e941";
}
.icon-help:before {
content: "\e919";
}
.icon-location:before {
content: "\e91b";
}
.icon-control-Map:before {
content: "\e91b";
}
.icon-type-Geolocation:before {
content: "\e91b";
}
.icon-logo:before {
content: "\e91c";
}
.icon-media:before {
content: "\e91d";
}
.icon-type-Assets:before {
content: "\e91d";
}
.icon-trigger-AssetChanged:before {
content: "\e91d";
}
.icon-type-UI:before { .icon-type-UI:before {
content: "\e975"; content: "\e975";
} }

39
tests/Squidex.Domain.Apps.Entities.Tests/Rules/ManualTriggerHandlerTests.cs

@ -0,0 +1,39 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Rules;
using Squidex.Infrastructure.EventSourcing;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Rules
{
public class ManualTriggerHandlerTests
{
private readonly IRuleTriggerHandler sut = new ManualTriggerHandler();
[Fact]
public async Task Should_create_event_with_name()
{
var envelope = Envelope.Create<AppEvent>(new RuleManuallyTriggered());
var result = await sut.CreateEnrichedEventAsync(envelope);
Assert.Equal("Manual", result.Name);
}
[Fact]
public void Should_always_trigger()
{
Assert.True(sut.Trigger(new EnrichedManualEvent(), new ManualTrigger()));
}
}
}

40
tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs

@ -66,25 +66,40 @@ namespace Squidex.Domain.Apps.Entities.Rules
} }
[Fact] [Fact]
public async Task Should_update_repositories_on_with_jobs_from_sender() public async Task Should_update_repository_when_enqueing()
{ {
var @event = Envelope.Create<IEvent>(new ContentCreated { AppId = appId }); var @event = Envelope.Create<IEvent>(new ContentCreated { AppId = appId });
var rule1 = new Rule(new ContentChangedTriggerV2(), new TestAction { Url = new Uri("https://squidex.io") }); var rule = CreateRule();
var rule2 = new Rule(new ContentChangedTriggerV2(), new TestAction { Url = new Uri("https://squidex.io") });
var job1 = new RuleJob { Created = now }; var job = new RuleJob { Created = now };
A.CallTo(() => ruleService.CreateJobAsync(rule.RuleDef, rule.Id, @event))
.Returns(job);
await sut.Enqueue(rule.RuleDef, rule.Id, @event);
A.CallTo(() => ruleEventRepository.EnqueueAsync(job, now))
.MustHaveHappened();
}
[Fact]
public async Task Should_update_repositories_with_jobs_from_service()
{
var @event = Envelope.Create<IEvent>(new ContentCreated { AppId = appId });
var ruleEntity1 = new RuleEntity { RuleDef = rule1 }; var rule1 = CreateRule();
var ruleEntity2 = new RuleEntity { RuleDef = rule2 }; var rule2 = CreateRule();
var job1 = new RuleJob { Created = now };
A.CallTo(() => appProvider.GetRulesAsync(appId.Id)) A.CallTo(() => appProvider.GetRulesAsync(appId.Id))
.Returns(new List<IRuleEntity> { ruleEntity1, ruleEntity2 }); .Returns(new List<IRuleEntity> { rule1, rule2 });
A.CallTo(() => ruleService.CreateJobAsync(rule1, ruleEntity1.Id, @event)) A.CallTo(() => ruleService.CreateJobAsync(rule1.RuleDef, rule1.Id, @event))
.Returns(job1); .Returns(job1);
A.CallTo(() => ruleService.CreateJobAsync(rule2, ruleEntity2.Id, @event)) A.CallTo(() => ruleService.CreateJobAsync(rule2.RuleDef, rule2.Id, @event))
.Returns(Task.FromResult<RuleJob>(null)); .Returns(Task.FromResult<RuleJob>(null));
await sut.On(@event); await sut.On(@event);
@ -92,5 +107,12 @@ namespace Squidex.Domain.Apps.Entities.Rules
A.CallTo(() => ruleEventRepository.EnqueueAsync(job1, now)) A.CallTo(() => ruleEventRepository.EnqueueAsync(job1, now))
.MustHaveHappened(); .MustHaveHappened();
} }
private static RuleEntity CreateRule()
{
var rule = new Rule(new ContentChangedTriggerV2(), new TestAction { Url = new Uri("https://squidex.io") });
return new RuleEntity { RuleDef = rule, Id = Guid.NewGuid() };
}
} }
} }

20
tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleGrainTests.cs

@ -17,6 +17,7 @@ using Squidex.Domain.Apps.Events.Rules;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Collections;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
using Xunit; using Xunit;
@ -25,6 +26,7 @@ namespace Squidex.Domain.Apps.Entities.Rules
public class RuleGrainTests : HandlerTestBase<RuleState> public class RuleGrainTests : HandlerTestBase<RuleState>
{ {
private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
private readonly IRuleEnqueuer ruleEnqueuer = A.Fake<IRuleEnqueuer>();
private readonly Guid ruleId = Guid.NewGuid(); private readonly Guid ruleId = Guid.NewGuid();
private readonly RuleGrain sut; private readonly RuleGrain sut;
@ -40,7 +42,7 @@ namespace Squidex.Domain.Apps.Entities.Rules
public RuleGrainTests() public RuleGrainTests()
{ {
sut = new RuleGrain(Store, A.Dummy<ISemanticLog>(), appProvider); sut = new RuleGrain(Store, A.Dummy<ISemanticLog>(), appProvider, ruleEnqueuer);
sut.ActivateAsync(Id).Wait(); sut.ActivateAsync(Id).Wait();
} }
@ -160,6 +162,22 @@ namespace Squidex.Domain.Apps.Entities.Rules
); );
} }
[Fact]
public async Task Trigger_should_invoke_rule_enqueue_but_not_change_snapshot()
{
var command = new TriggerRule();
await ExecuteCreateAsync();
var result = await sut.ExecuteAsync(CreateRuleCommand(command));
Assert.Null(result.Value);
A.CallTo(() => ruleEnqueuer.Enqueue(sut.Snapshot.RuleDef, sut.Id,
A<Envelope<IEvent>>.That.Matches(x => x.Payload is RuleManuallyTriggered)))
.MustHaveHappened();
}
private Task ExecuteCreateAsync() private Task ExecuteCreateAsync()
{ {
return sut.ExecuteAsync(CreateRuleCommand(MakeCreateCommand())); return sut.ExecuteAsync(CreateRuleCommand(MakeCreateCommand()));

Loading…
Cancel
Save