diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchema.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchema.cs index 600fcaf6e..439ead886 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchema.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchema.cs @@ -20,6 +20,6 @@ namespace Squidex.Domain.Apps.Core.Rules.Triggers public bool SendDelete { get; set; } - public bool SendPublish { get; set; } + public bool SendChange { get; set; } } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs index e0b8ef424..e65686d8e 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs @@ -43,7 +43,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Triggers (schema.SendCreate && @event is ContentCreated) || (schema.SendUpdate && @event is ContentUpdated) || (schema.SendDelete && @event is ContentDeleted) || - (schema.SendPublish && @event is ContentStatusChanged statusChanged && statusChanged.Status == Status.Published); + (schema.SendChange && @event is ContentStatusChanged statusChanged && statusChanged.Status == Status.Published); } } } diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository_EventHandling.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository_EventHandling.cs index db2e3db37..26be2002a 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository_EventHandling.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository_EventHandling.cs @@ -25,7 +25,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Rules public string EventsFilter { - get { return "^rules-"; } + get { return "^rule-"; } } public Task On(Envelope @event) diff --git a/src/Squidex.Domain.Apps.Write/Rules/Commands/CreateRule.cs b/src/Squidex.Domain.Apps.Write/Rules/Commands/CreateRule.cs index 34aaa4d21..1c6bafd7e 100644 --- a/src/Squidex.Domain.Apps.Write/Rules/Commands/CreateRule.cs +++ b/src/Squidex.Domain.Apps.Write/Rules/Commands/CreateRule.cs @@ -6,9 +6,15 @@ // All rights reserved. // ========================================================================== +using System; + namespace Squidex.Domain.Apps.Write.Rules.Commands { public sealed class CreateRule : RuleEditCommand { + public CreateRule() + { + RuleId = Guid.NewGuid(); + } } } diff --git a/src/Squidex/Controllers/Api/Rules/Models/Triggers/ContentChangedTriggerSchemaDto.cs b/src/Squidex/Controllers/Api/Rules/Models/Triggers/ContentChangedTriggerSchemaDto.cs index 29e0c71fd..05215c376 100644 --- a/src/Squidex/Controllers/Api/Rules/Models/Triggers/ContentChangedTriggerSchemaDto.cs +++ b/src/Squidex/Controllers/Api/Rules/Models/Triggers/ContentChangedTriggerSchemaDto.cs @@ -35,6 +35,6 @@ namespace Squidex.Controllers.Api.Rules.Models.Triggers /// /// True, when to send a message for published events. /// - public bool SendPublish { get; set; } + public bool SendChange { get; set; } } } diff --git a/src/Squidex/app/app.routes.ts b/src/Squidex/app/app.routes.ts index 3b4e8fea7..2505c716a 100644 --- a/src/Squidex/app/app.routes.ts +++ b/src/Squidex/app/app.routes.ts @@ -64,8 +64,8 @@ export const routes: Routes = [ loadChildren: './features/assets/module#SqxFeatureAssetsModule' }, { - path: 'webhooks', - loadChildren: './features/webhooks/module#SqxFeatureWebhooksModule' + path: 'rules', + loadChildren: './features/rules/module#SqxFeatureRulesModule' }, { path: 'settings', diff --git a/src/Squidex/app/features/apps/pages/apps-page.component.html b/src/Squidex/app/features/apps/pages/apps-page.component.html index e485a137e..bb08afd66 100644 --- a/src/Squidex/app/features/apps/pages/apps-page.component.html +++ b/src/Squidex/app/features/apps/pages/apps-page.component.html @@ -31,7 +31,8 @@ diff --git a/src/Squidex/app/features/rules/declarations.ts b/src/Squidex/app/features/rules/declarations.ts new file mode 100644 index 000000000..c2f70d3ef --- /dev/null +++ b/src/Squidex/app/features/rules/declarations.ts @@ -0,0 +1,13 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +export * from './pages/rules/actions/webhook-action.component'; +export * from './pages/rules/triggers/content-changed-trigger.component'; +export * from './pages/rules/rule-wizard.component'; +export * from './pages/rules/rules-page.component'; + +export * from './pages/events/rule-events-page.component'; \ No newline at end of file diff --git a/src/Squidex/app/features/webhooks/index.ts b/src/Squidex/app/features/rules/index.ts similarity index 100% rename from src/Squidex/app/features/webhooks/index.ts rename to src/Squidex/app/features/rules/index.ts diff --git a/src/Squidex/app/features/webhooks/module.ts b/src/Squidex/app/features/rules/module.ts similarity index 53% rename from src/Squidex/app/features/webhooks/module.ts rename to src/Squidex/app/features/rules/module.ts index 12a58e960..23a869ede 100644 --- a/src/Squidex/app/features/webhooks/module.ts +++ b/src/Squidex/app/features/rules/module.ts @@ -9,32 +9,26 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { - HelpComponent, SqxFrameworkModule, SqxSharedModule } from 'shared'; import { - WebhookComponent, - WebhookEventsPageComponent, - WebhooksPageComponent + ContentChangedTriggerComponent, + RuleEventsPageComponent, + RulesPageComponent, + RuleWizardComponent, + WebhookActionComponent } from './declarations'; const routes: Routes = [ { path: '', - component: WebhooksPageComponent, + component: RulesPageComponent, children: [ { path: 'events', - component: WebhookEventsPageComponent - }, - { - path: 'help', - component: HelpComponent, - data: { - helpPage: '05-integrated/webhooks' - } + component: RuleEventsPageComponent } ] } @@ -47,9 +41,11 @@ const routes: Routes = [ RouterModule.forChild(routes) ], declarations: [ - WebhookComponent, - WebhookEventsPageComponent, - WebhooksPageComponent + ContentChangedTriggerComponent, + RuleEventsPageComponent, + RulesPageComponent, + RuleWizardComponent, + WebhookActionComponent ] }) -export class SqxFeatureWebhooksModule { } \ No newline at end of file +export class SqxFeatureRulesModule { } \ No newline at end of file diff --git a/src/Squidex/app/features/webhooks/pages/webhook-events-page.component.html b/src/Squidex/app/features/rules/pages/events/rule-events-page.component.html similarity index 96% rename from src/Squidex/app/features/webhooks/pages/webhook-events-page.component.html rename to src/Squidex/app/features/rules/pages/events/rule-events-page.component.html index b68b5b51c..9bb9ebd9f 100644 --- a/src/Squidex/app/features/webhooks/pages/webhook-events-page.component.html +++ b/src/Squidex/app/features/rules/pages/events/rule-events-page.component.html @@ -1,4 +1,4 @@ - +
@@ -57,7 +57,7 @@ {{event.eventName}} - {{event.requestUrl}} + {{event.description}} {{event.created | sqxFromNow}} diff --git a/src/Squidex/app/features/webhooks/pages/webhook-events-page.component.scss b/src/Squidex/app/features/rules/pages/events/rule-events-page.component.scss similarity index 100% rename from src/Squidex/app/features/webhooks/pages/webhook-events-page.component.scss rename to src/Squidex/app/features/rules/pages/events/rule-events-page.component.scss diff --git a/src/Squidex/app/features/webhooks/pages/webhook-events-page.component.ts b/src/Squidex/app/features/rules/pages/events/rule-events-page.component.ts similarity index 71% rename from src/Squidex/app/features/webhooks/pages/webhook-events-page.component.ts rename to src/Squidex/app/features/rules/pages/events/rule-events-page.component.ts index 53bed278e..6e5fe2657 100644 --- a/src/Squidex/app/features/webhooks/pages/webhook-events-page.component.ts +++ b/src/Squidex/app/features/rules/pages/events/rule-events-page.component.ts @@ -14,23 +14,23 @@ import { DialogService, ImmutableArray, Pager, - WebhookEventDto, - WebhooksService + RuleEventDto, + RulesService } from 'shared'; @Component({ - selector: 'sqx-webhook-events-page', - styleUrls: ['./webhook-events-page.component.scss'], - templateUrl: './webhook-events-page.component.html' + selector: 'sqx-rule-events-page', + styleUrls: ['./rule-events-page.component.scss'], + templateUrl: './rule-events-page.component.html' }) -export class WebhookEventsPageComponent extends AppComponentBase implements OnInit { - public eventsItems = ImmutableArray.empty(); +export class RuleEventsPageComponent extends AppComponentBase implements OnInit { + public eventsItems = ImmutableArray.empty(); public eventsPager = new Pager(0); public selectedEventId: string | null = null; constructor(dialogs: DialogService, appsStore: AppsStoreService, authService: AuthService, - private readonly webhooksService: WebhooksService + private readonly rulesService: RulesService ) { super(dialogs, appsStore, authService); } @@ -41,7 +41,7 @@ export class WebhookEventsPageComponent extends AppComponentBase implements OnIn public load(showInfo = false) { this.appNameOnce() - .switchMap(app => this.webhooksService.getEvents(app, this.eventsPager.pageSize, this.eventsPager.skip)) + .switchMap(app => this.rulesService.getEvents(app, this.eventsPager.pageSize, this.eventsPager.skip)) .subscribe(dtos => { this.eventsItems = ImmutableArray.of(dtos.items); this.eventsPager = this.eventsPager.setCount(dtos.total); @@ -54,11 +54,11 @@ export class WebhookEventsPageComponent extends AppComponentBase implements OnIn }); } - public enqueueEvent(event: WebhookEventDto) { + public enqueueEvent(event: RuleEventDto) { this.appNameOnce() - .switchMap(app => this.webhooksService.enqueueEvent(app, event.id)) + .switchMap(app => this.rulesService.enqueueEvent(app, event.id)) .subscribe(() => { - this.notifyInfo('Events enqueued. Will be send in a few seconds.'); + this.notifyInfo('Events enqueued. Will be resend in a few seconds.'); }, error => { this.notifyError(error); }); diff --git a/src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.html b/src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.html new file mode 100644 index 000000000..62f2231d5 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.html @@ -0,0 +1,29 @@ +
+
+ + +
+ + + + + + The url where the events will be sent to. + +
+
+ +
+ + +
+ + + + + + The shared secret will be used to add a header X-Signature=Sha256(RequestBody + Secret) + +
+
+
\ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.scss b/src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.scss new file mode 100644 index 000000000..fbb752506 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.scss @@ -0,0 +1,2 @@ +@import '_vars'; +@import '_mixins'; \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.ts b/src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.ts new file mode 100644 index 000000000..d11b3e8c8 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.ts @@ -0,0 +1,55 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { FormBuilder, Validators } from '@angular/forms'; + +@Component({ + selector: 'sqx-webhook-action', + styleUrls: ['./webhook-action.component.scss'], + templateUrl: './webhook-action.component.html' +}) +export class WebhookActionComponent implements OnInit { + @Input() + public action: any; + + @Output() + public actionChanged = new EventEmitter(); + + public actionFormSubmitted = false; + public actionForm = + this.formBuilder.group({ + url: ['', + [ + Validators.required + ]], + sharedSecret: [''] + }); + + constructor( + private readonly formBuilder: FormBuilder + ) { + } + + public ngOnInit() { + this.action = Object.assign({}, { url: '', sharedSecret: '' }, this.action || {}); + + this.actionFormSubmitted = false; + this.actionForm.reset(); + this.actionForm.setValue(this.action); + } + + public save() { + this.actionFormSubmitted = true; + + if (this.actionForm.valid) { + const action = this.actionForm.value; + + this.actionChanged.emit(action); + } + } +} \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html b/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html new file mode 100644 index 000000000..bf9f21d62 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html @@ -0,0 +1,79 @@ + \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.scss b/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.scss new file mode 100644 index 000000000..d17243924 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.scss @@ -0,0 +1,38 @@ +@import '_vars'; +@import '_mixins'; + +.modal { + &-dialog { + height: 70%; + } + + &-content { + min-height: 100%; + max-height: 100%; + } + + &-body { + overflow-y: auto; + } + + &-header { + @include flex-shrink(0); + } + + &-footer { + @include flex-shrink(0); + } + + &-form { + padding-top: 2em; + padding-bottom: 0; + } +} + +.clearfix { + width: 100%; +} + +.rule-element { + margin-right: .5rem; +} \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.ts b/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.ts new file mode 100644 index 000000000..019951711 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.ts @@ -0,0 +1,96 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core'; + +import { + AppComponentBase, + AppsStoreService, + AuthService, + CreateRuleDto, + DateTime, + DialogService, + fadeAnimation, + ruleActions, + ruleTriggers, + RuleDto, + RulesService, + SchemaDto +} from 'shared'; + +@Component({ + selector: 'sqx-rule-wizard', + styleUrls: ['./rule-wizard.component.scss'], + templateUrl: './rule-wizard.component.html', + animations: [ + fadeAnimation + ] +}) +export class RuleWizardComponent extends AppComponentBase { + public ruleActions = ruleActions; + public ruleTriggers = ruleTriggers; + + public triggerType: string; + public trigger: any = {}; + public actionType: string; + public action: any = {}; + public step = 1; + + @ViewChild('triggerControl') + public triggerControl: any; + + @ViewChild('actionControl') + public actionControl: any; + + @Output() + public cancelled = new EventEmitter(); + + @Output() + public created = new EventEmitter(); + + @Input() + public schemas: SchemaDto[]; + + constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService, + private readonly rulesService: RulesService + ) { + super(dialogs, apps, authService); + } + + public selectTriggerType(type: string) { + this.triggerType = type; + this.step++; + } + + public selectTrigger(value: any) { + this.trigger = Object.assign({}, value, { triggerType: this.triggerType }); + this.step++; + } + + public selectActionType(type: string) { + this.actionType = type; + this.step++; + } + + public selectAction(value: any) { + this.action = Object.assign({}, value, { actionType: this.actionType }); + + const requestDto = new CreateRuleDto(this.trigger, this.action); + + this.appNameOnce() + .switchMap(app => this.rulesService.postRule(app, requestDto, this.authService.user!.id, DateTime.now())) + .subscribe(dto => { + this.created.emit(dto); + }, error => { + this.notifyError(error); + }); + } + + public cancel() { + this.cancelled.emit(); + } +} \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/rules-page.component.html b/src/Squidex/app/features/rules/pages/rules/rules-page.component.html new file mode 100644 index 000000000..57dc83714 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/rules-page.component.html @@ -0,0 +1,51 @@ + + + +
+
+
+ + + + + + +
+ +

Rules

+
+ + + + +
+ +
+
+
+ No Rule created yet. +
+
+
+ + +
+ + + +
+
+ + + + \ No newline at end of file diff --git a/src/Squidex/app/features/webhooks/pages/webhooks-page.component.scss b/src/Squidex/app/features/rules/pages/rules/rules-page.component.scss similarity index 100% rename from src/Squidex/app/features/webhooks/pages/webhooks-page.component.scss rename to src/Squidex/app/features/rules/pages/rules/rules-page.component.scss diff --git a/src/Squidex/app/features/rules/pages/rules/rules-page.component.ts b/src/Squidex/app/features/rules/pages/rules/rules-page.component.ts new file mode 100644 index 000000000..212be1d14 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/rules-page.component.ts @@ -0,0 +1,66 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Component, OnInit } from '@angular/core'; + +import { + AppComponentBase, + AppsStoreService, + AuthService, + DialogService, + fadeAnimation, + ImmutableArray, + ModalView, + RuleDto, + RulesService, + SchemaDto, + SchemasService +} from 'shared'; + +@Component({ + selector: 'sqx-rules-page', + styleUrls: ['./rules-page.component.scss'], + templateUrl: './rules-page.component.html', + animations: [ + fadeAnimation + ] +}) +export class RulesPageComponent extends AppComponentBase implements OnInit { + public addRuleDialog = new ModalView(true, false); + + public rules: ImmutableArray; + public schemas: SchemaDto[]; + + constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService, + private readonly schemasService: SchemasService, + private readonly rulesService: RulesService + ) { + super(dialogs, apps, authService); + } + + public ngOnInit() { + this.load(); + } + + public load(showInfo = false) { + this.appNameOnce() + .switchMap(app => + this.schemasService.getSchemas(app) + .combineLatest(this.rulesService.getRules(app), + (s, w) => { return { rules: w, schemas: s }; })) + .subscribe(dtos => { + this.schemas = dtos.schemas; + this.rules = ImmutableArray.of(dtos.rules); + + if (showInfo) { + this.notifyInfo('Rules reloaded.'); + } + }, error => { + this.notifyError(error); + }); + } +} diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html b/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html new file mode 100644 index 000000000..ff7321f40 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Schema + + All + + C + + U + + D + + C +
+ {{schema.schema.name}} + + + + + + + + + + + + +
+ +
+
+
+ +
+ + +
+
\ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.scss b/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.scss new file mode 100644 index 000000000..4869322fb --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.scss @@ -0,0 +1,8 @@ +@import '_vars'; +@import '_mixins'; + +.section { + border-top: 1px solid $color-border; + padding-top: 1rem; + padding-bottom: 0; +} \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts b/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts new file mode 100644 index 000000000..cecb4385f --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts @@ -0,0 +1,147 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; + +import { + ImmutableArray, + SchemaDto +} from 'shared'; + +export interface TriggerSchemaForm { + schema: SchemaDto; + sendAll: boolean; + sendCreate: boolean; + sendUpdate: boolean; + sendDelete: boolean; + sendChange: boolean; +} + +@Component({ + selector: 'sqx-content-changed-trigger', + styleUrls: ['./content-changed-trigger.component.scss'], + templateUrl: './content-changed-trigger.component.html' +}) +export class ContentChangedTriggerComponent implements OnInit { + @Input() + public schemas: SchemaDto[]; + + @Input() + public trigger: any; + + @Output() + public triggerChanged = new EventEmitter(); + + public triggerSchemas: ImmutableArray; + + public schemaToAdd: SchemaDto; + public schemasToAdd: ImmutableArray; + + public get hasSchema() { + return !!this.schemaToAdd; + } + + public ngOnInit() { + const triggerSchemas: any[] = (this.trigger.schemas = this.trigger.schemas || []); + + this.triggerSchemas = + ImmutableArray.of( + triggerSchemas.map(triggerSchema => { + const schema = this.schemas.find(s => s.id === triggerSchema.schemaId); + + if (schema) { + return this.updateSendAll({ + schema: schema, + sendAll: false, + sendCreate: triggerSchema.sendCreate, + sendUpdate: triggerSchema.sendUpdate, + sendDelete: triggerSchema.sendDelete, + sendChange: triggerSchema.sendChange + }); + } else { + return null; + } + }).filter(s => s !== null).map(s => s!)).sortByStringAsc(s => s.schema.name); + + this.schemasToAdd = + ImmutableArray.of( + this.schemas.filter(schema => + !triggerSchemas.find(s => s.schemaId === schema.id))) + .sortByStringAsc(x => x.name); + + this.schemaToAdd = this.schemasToAdd.values[0]; + } + + public save() { + const schemas = + this.triggerSchemas.values.map(s => { + return { + schemaId: s.schema.id, + sendCreate: s.sendCreate, + sendUpdate: s.sendUpdate, + sendDelete: s.sendDelete, + sendChange: s.sendChange + }; + }); + + this.triggerChanged.emit({ schemas }); + } + + public removeSchema(schemaForm: TriggerSchemaForm) { + this.triggerSchemas = this.triggerSchemas.remove(schemaForm); + + this.schemasToAdd = this.schemasToAdd.push(schemaForm.schema).sortByStringAsc(x => x.name); + this.schemaToAdd = this.schemasToAdd.values[0]; + } + + public addSchema() { + this.triggerSchemas = + this.triggerSchemas.push( + this.updateSendAll({ + schema: this.schemaToAdd, + sendAll: false, + sendCreate: false, + sendUpdate: false, + sendDelete: false, + sendChange: false + })).sortByStringAsc(x => x.schema.name); + + this.schemasToAdd = this.schemasToAdd.remove(this.schemaToAdd).sortByStringAsc(x => x.name); + this.schemaToAdd = this.schemasToAdd.values[0]; + } + + public toggle(schemaForm: TriggerSchemaForm, property: string) { + const newSchema = this.updateSendAll(Object.assign({}, schemaForm, { [property]: !schemaForm[property] })); + + this.triggerSchemas = this.triggerSchemas.replace(schemaForm, newSchema); + } + + public toggleAll(schemaForm: TriggerSchemaForm) { + const newSchema = this.updateAll({ schema: schemaForm.schema }, !schemaForm.sendAll); + + this.triggerSchemas = this.triggerSchemas.replace(schemaForm, newSchema); + } + + private updateAll(schemaForm: TriggerSchemaForm, value: boolean): TriggerSchemaForm { + schemaForm.sendAll = value; + schemaForm.sendCreate = value; + schemaForm.sendUpdate = value; + schemaForm.sendDelete = value; + schemaForm.sendChange = value; + return schemaForm; + } + + private updateSendAll(schemaForm: TriggerSchemaForm): TriggerSchemaForm { + schemaForm.sendAll = + schemaForm.sendCreate && + schemaForm.sendUpdate && + schemaForm.sendDelete && + schemaForm.sendChange; + + return schemaForm; + } +} \ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html b/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html index 29831ee93..05c321f29 100644 --- a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html +++ b/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html @@ -66,7 +66,10 @@ diff --git a/src/Squidex/app/features/webhooks/declarations.ts b/src/Squidex/app/features/webhooks/declarations.ts deleted file mode 100644 index 84b17769c..000000000 --- a/src/Squidex/app/features/webhooks/declarations.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Squidex Headless CMS - * - * @license - * Copyright (c) Sebastian Stehle. All rights reserved - */ - -export * from './pages/webhook-events-page.component'; -export * from './pages/webhook.component'; -export * from './pages/webhooks-page.component'; \ No newline at end of file diff --git a/src/Squidex/app/features/webhooks/pages/webhook.component.html b/src/Squidex/app/features/webhooks/pages/webhook.component.html deleted file mode 100644 index 2f015bbb6..000000000 --- a/src/Squidex/app/features/webhooks/pages/webhook.component.html +++ /dev/null @@ -1,149 +0,0 @@ -
-
- - - - - - - - - - - - - - - - - - - - - -
- - - -
Url: - - - -
Secret: - - - -
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Schema - - All - - C - - U - - D - - P -
- {{schema.schema.name}} - - - - - - - - - - - - -
-
- -
-
-
- -
- - -
-
- -
-
-
- - {{webhook.totalSucceeded}} - -
-
- - {{webhook.totalFailed}} - -
-
- - {{webhook.totalTimedout}} - -
-
- - {{webhook.averageRequestTimeMs}} ms - -
-
-
-
\ No newline at end of file diff --git a/src/Squidex/app/features/webhooks/pages/webhook.component.scss b/src/Squidex/app/features/webhooks/pages/webhook.component.scss deleted file mode 100644 index 61873b1a2..000000000 --- a/src/Squidex/app/features/webhooks/pages/webhook.component.scss +++ /dev/null @@ -1,13 +0,0 @@ -@import '_vars'; -@import '_mixins'; - -.schemas-control { - width: 18rem; -} - -.webhook-section { - border-top: 1px solid $color-border; - margin-left: -1.25rem; - margin-right: -1.25rem; - padding: 1rem 1.25rem 0; -} \ No newline at end of file diff --git a/src/Squidex/app/features/webhooks/pages/webhook.component.ts b/src/Squidex/app/features/webhooks/pages/webhook.component.ts deleted file mode 100644 index 7ac843b2d..000000000 --- a/src/Squidex/app/features/webhooks/pages/webhook.component.ts +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Squidex Headless CMS - * - * @license - * Copyright (c) Sebastian Stehle. All rights reserved - */ - -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { FormBuilder, Validators } from '@angular/forms'; - -import { - ImmutableArray, - SchemaDto, - UpdateWebhookDto, - WebhookDto, - WebhookSchemaDto -} from 'shared'; - -export interface WebhookSchemaForm { - schema: SchemaDto; - sendAll: boolean; - sendCreate: boolean; - sendUpdate: boolean; - sendDelete: boolean; - sendPublish: boolean; -} - -@Component({ - selector: 'sqx-webhook', - styleUrls: ['./webhook.component.scss'], - templateUrl: './webhook.component.html' -}) -export class WebhookComponent implements OnInit { - @Output() - public deleting = new EventEmitter(); - - @Output() - public updating = new EventEmitter(); - - @Input() - public allSchemas: SchemaDto[]; - - @Input() - public webhook: WebhookDto; - - public schemas: ImmutableArray; - - public schemaToAdd: SchemaDto; - public schemasToAdd: ImmutableArray; - - public webhookForm = - this.formBuilder.group({ - url: ['', - [ - Validators.required - ]] - }); - - public get hasUrl() { - return this.webhookForm.controls['url'].value && this.webhookForm.controls['url'].value.length > 0; - } - - public get hasSchema() { - return !!this.schemaToAdd; - } - - constructor( - private readonly formBuilder: FormBuilder - ) { - } - - public ngOnInit() { - this.webhookForm.controls['url'].setValue(this.webhook.url); - - this.schemas = - ImmutableArray.of( - this.webhook.schemas.map(webhookSchema => { - const schema = this.allSchemas.find(s => s.id === webhookSchema.schemaId); - - if (schema) { - return this.updateSendAll({ - schema: schema, - sendAll: false, - sendCreate: webhookSchema.sendCreate, - sendUpdate: webhookSchema.sendUpdate, - sendDelete: webhookSchema.sendDelete, - sendPublish: webhookSchema.sendPublish - }); - } else { - return null; - } - }).filter(w => w !== null).map(w => w!)).sortByStringAsc(x => x.schema.name); - - this.schemasToAdd = - ImmutableArray.of( - this.allSchemas.filter(schema => - !this.webhook.schemas.find(w => w.schemaId === schema.id))) - .sortByStringAsc(x => x.name); - this.schemaToAdd = this.schemasToAdd.values[0]; - } - - public removeSchema(schemaForm: WebhookSchemaForm) { - this.schemas = this.schemas.remove(schemaForm); - - this.schemasToAdd = this.schemasToAdd.push(schemaForm.schema).sortByStringAsc(x => x.name); - this.schemaToAdd = this.schemasToAdd.values[0]; - } - - public addSchema() { - this.schemas = - this.schemas.push( - this.updateSendAll({ - schema: this.schemaToAdd, - sendAll: false, - sendCreate: false, - sendUpdate: false, - sendDelete: false, - sendPublish: false - })).sortByStringAsc(x => x.schema.name); - - this.schemasToAdd = this.schemasToAdd.remove(this.schemaToAdd).sortByStringAsc(x => x.name); - this.schemaToAdd = this.schemasToAdd.values[0]; - } - - public save() { - const requestDto = - new UpdateWebhookDto( - this.webhookForm.controls['url'].value, - this.schemas.values.map(schema => - new WebhookSchemaDto( - schema.schema.id, - schema.sendCreate, - schema.sendUpdate, - schema.sendDelete, - schema.sendPublish))); - - this.emitUpdating(requestDto); - } - - public toggle(schemaForm: WebhookSchemaForm, property: string) { - const newSchema = this.updateSendAll(Object.assign({}, schemaForm, { [property]: !schemaForm[property] })); - - this.schemas = this.schemas.replace(schemaForm, newSchema); - } - - public toggleAll(schemaForm: WebhookSchemaForm) { - const newSchema = this.updateAll({ schema: schemaForm.schema }, !schemaForm.sendAll); - - this.schemas = this.schemas.replace(schemaForm, newSchema); - } - - private emitUpdating(dto: UpdateWebhookDto) { - this.updating.emit(dto); - } - - private updateAll(schemaForm: WebhookSchemaForm, value: boolean): WebhookSchemaForm { - schemaForm.sendAll = value; - schemaForm.sendCreate = value; - schemaForm.sendUpdate = value; - schemaForm.sendDelete = value; - schemaForm.sendPublish = value; - return schemaForm; - } - - private updateSendAll(schemaForm: WebhookSchemaForm): WebhookSchemaForm { - schemaForm.sendAll = - schemaForm.sendCreate && - schemaForm.sendUpdate && - schemaForm.sendDelete && - schemaForm.sendPublish; - - return schemaForm; - } -} diff --git a/src/Squidex/app/features/webhooks/pages/webhooks-page.component.html b/src/Squidex/app/features/webhooks/pages/webhooks-page.component.html deleted file mode 100644 index e49cc91eb..000000000 --- a/src/Squidex/app/features/webhooks/pages/webhooks-page.component.html +++ /dev/null @@ -1,72 +0,0 @@ - - - -
-
-
- - - -
- -

Webhooks

-
- - - - -
- -
-
-
- No Webhook created yet. -
- -
- - - -
-
- -
- - - - - - - - - The sidebar navigation contains useful context specific links. Here you can view the history how this schema has changed over time. - - - - Click the help icon to show a context specific help page. Go to https://docs.squidex.io for the full documentation. - -
-
-
- - \ No newline at end of file diff --git a/src/Squidex/app/features/webhooks/pages/webhooks-page.component.ts b/src/Squidex/app/features/webhooks/pages/webhooks-page.component.ts deleted file mode 100644 index 6661a4dd3..000000000 --- a/src/Squidex/app/features/webhooks/pages/webhooks-page.component.ts +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Squidex Headless CMS - * - * @license - * Copyright (c) Sebastian Stehle. All rights reserved - */ - -import { Component, OnInit } from '@angular/core'; -import { FormBuilder, Validators } from '@angular/forms'; - -import { - AppComponentBase, - AppsStoreService, - AuthService, - CreateWebhookDto, - DateTime, - DialogService, - ImmutableArray, - SchemaDto, - SchemasService, - WebhookDto, - WebhooksService, - UpdateWebhookDto -} from 'shared'; - -@Component({ - selector: 'sqx-webhooks-page', - styleUrls: ['./webhooks-page.component.scss'], - templateUrl: './webhooks-page.component.html' -}) -export class WebhooksPageComponent extends AppComponentBase implements OnInit { - public webhooks: ImmutableArray; - public schemas: SchemaDto[]; - - public addWebhookFormSubmitted = false; - public addWebhookForm = - this.formBuilder.group({ - url: ['', - [ - Validators.required - ]] - }); - - public get hasUrl() { - return this.addWebhookForm.controls['url'].value && this.addWebhookForm.controls['url'].value.length > 0; - } - - constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService, - private readonly schemasService: SchemasService, - private readonly webhooksService: WebhooksService, - private readonly formBuilder: FormBuilder - ) { - super(dialogs, apps, authService); - } - - public ngOnInit() { - this.load(); - } - - public load(showInfo = false) { - this.appNameOnce() - .switchMap(app => - this.schemasService.getSchemas(app) - .combineLatest(this.webhooksService.getWebhooks(app), - (s, w) => { return { webhooks: w, schemas: s }; })) - .subscribe(dtos => { - this.schemas = dtos.schemas; - this.webhooks = ImmutableArray.of(dtos.webhooks); - - if (showInfo) { - this.notifyInfo('Webhooks reloaded.'); - } - }, error => { - this.notifyError(error); - }); - } - - public deleteWebhook(webhook: WebhookDto) { - this.appNameOnce() - .switchMap(app => this.webhooksService.deleteWebhook(app, webhook.id, webhook.version)) - .subscribe(dto => { - this.webhooks = this.webhooks.remove(webhook); - }, error => { - this.notifyError(error); - }); - } - - public updateWebhook(webhook: WebhookDto, requestDto: UpdateWebhookDto) { - this.appNameOnce() - .switchMap(app => this.webhooksService.putWebhook(app, webhook.id, requestDto, webhook.version)) - .subscribe(dto => { - this.webhooks = this.webhooks.replace(webhook, webhook.update(requestDto, this.userToken, dto.version)); - - this.notifyInfo('Webhook saved.'); - }, error => { - this.notifyError(error); - }); - } - - public addWebhook() { - this.addWebhookFormSubmitted = true; - - if (this.addWebhookForm.valid) { - this.addWebhookForm.disable(); - - const requestDto = new CreateWebhookDto(this.addWebhookForm.controls['url'].value, []); - - const me = this.userToken; - - this.appNameOnce() - .switchMap(app => this.webhooksService.postWebhook(app, requestDto, me, DateTime.now())) - .subscribe(dto => { - this.webhooks = this.webhooks.push(dto); - - this.resetWebhookForm(); - }, error => { - this.notifyError(error); - this.enableWebhookForm(); - }); - } - } - - public cancelAddWebhook() { - this.resetWebhookForm(); - } - - private enableWebhookForm() { - this.addWebhookForm.enable(); - } - - private resetWebhookForm() { - this.addWebhookFormSubmitted = false; - this.addWebhookForm.enable(); - this.addWebhookForm.reset(); - } -} diff --git a/src/Squidex/app/framework/angular/keys.pipe.spec.ts b/src/Squidex/app/framework/angular/keys.pipe.spec.ts new file mode 100644 index 000000000..09de18928 --- /dev/null +++ b/src/Squidex/app/framework/angular/keys.pipe.spec.ts @@ -0,0 +1,26 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { + KeysPipe +} from './keys.pipe'; + +describe('KeysPipe', () => { + it('should return keys', () => { + const value = { + key1: 1, + key2: 2 + }; + + const pipe = new KeysPipe(); + + const actual = pipe.transform(value); + const expected = ['key1', 'key2']; + + expect(actual).toBe(expected); + }); +}); \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/keys.pipe.ts b/src/Squidex/app/framework/angular/keys.pipe.ts new file mode 100644 index 000000000..8b3b91334 --- /dev/null +++ b/src/Squidex/app/framework/angular/keys.pipe.ts @@ -0,0 +1,18 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'sqxKeys', + pure: true +}) +export class KeysPipe implements PipeTransform { + public transform(value: any, args: any[] = null): any { + return Object.keys(value); + } +} \ No newline at end of file diff --git a/src/Squidex/app/framework/declarations.ts b/src/Squidex/app/framework/declarations.ts index 40c8b1d87..f2883f9b8 100644 --- a/src/Squidex/app/framework/declarations.ts +++ b/src/Squidex/app/framework/declarations.ts @@ -23,6 +23,7 @@ export * from './angular/image-source.directive'; export * from './angular/indeterminate-value.directive'; export * from './angular/jscript-editor.component'; export * from './angular/json-editor.component'; +export * from './angular/keys.pipe'; export * from './angular/lowercase-input.directive'; export * from './angular/markdown-editor.component'; export * from './angular/modal-target.directive'; diff --git a/src/Squidex/app/framework/module.ts b/src/Squidex/app/framework/module.ts index 973635904..ad8b62490 100644 --- a/src/Squidex/app/framework/module.ts +++ b/src/Squidex/app/framework/module.ts @@ -36,6 +36,7 @@ import { IndeterminateValueDirective, JscriptEditorComponent, JsonEditorComponent, + KeysPipe, KNumberPipe, LocalCacheService, LocalStoreService, @@ -102,6 +103,7 @@ import { IndeterminateValueDirective, JscriptEditorComponent, JsonEditorComponent, + KeysPipe, KNumberPipe, LowerCaseInputDirective, MarkdownEditorComponent, @@ -151,6 +153,7 @@ import { IndeterminateValueDirective, JscriptEditorComponent, JsonEditorComponent, + KeysPipe, KNumberPipe, LowerCaseInputDirective, MarkdownEditorComponent, diff --git a/src/Squidex/app/shared/declarations-base.ts b/src/Squidex/app/shared/declarations-base.ts index 01d4a0bdc..7b73101d9 100644 --- a/src/Squidex/app/shared/declarations-base.ts +++ b/src/Squidex/app/shared/declarations-base.ts @@ -30,12 +30,12 @@ export * from './services/help.service'; export * from './services/history.service'; export * from './services/languages.service'; export * from './services/plans.service'; +export * from './services/rules.service'; export * from './services/schemas.service'; export * from './services/ui.service'; export * from './services/usages.service'; export * from './services/users-provider.service'; export * from './services/users.service'; -export * from './services/webhooks.service'; export * from './utils/messages'; diff --git a/src/Squidex/app/shared/module.ts b/src/Squidex/app/shared/module.ts index f67368770..890bfb326 100644 --- a/src/Squidex/app/shared/module.ts +++ b/src/Squidex/app/shared/module.ts @@ -44,6 +44,7 @@ import { ResolveSchemaGuard, SchemasService, ResolveUserGuard, + RulesService, UIService, UsagesService, UserDtoPicture, @@ -56,8 +57,7 @@ import { UserPictureRefPipe, UserManagementService, UsersProviderService, - UsersService, - WebhooksService + UsersService } from './declarations'; @NgModule({ @@ -129,13 +129,13 @@ export class SqxSharedModule { ResolvePublishedSchemaGuard, ResolveSchemaGuard, ResolveUserGuard, + RulesService, SchemasService, UIService, UsagesService, UserManagementService, UsersProviderService, UsersService, - WebhooksService, { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, diff --git a/src/Squidex/app/shared/services/rules.service.spec.ts b/src/Squidex/app/shared/services/rules.service.spec.ts new file mode 100644 index 000000000..0e1e7570a --- /dev/null +++ b/src/Squidex/app/shared/services/rules.service.spec.ts @@ -0,0 +1,310 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + + +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { inject, TestBed } from '@angular/core/testing'; + +import { + AnalyticsService, + ApiUrlConfig, + CreateRuleDto, + DateTime, + UpdateRuleDto, + Version, + RuleDto, + RuleEventDto, + RuleEventsDto, + RulesService +} from './../'; + +describe('RuleDto', () => { + const creation = DateTime.today(); + const creator = 'not-me'; + const modified = DateTime.now(); + const modifier = 'me'; + const version = new Version('1'); + const newVersion = new Version('2'); + + it('should update trigger and action', () => { + const update = new UpdateRuleDto({ param1: 1, triggerType: 'NewType' }, { param2: 2, actionType: 'NewType' }); + + const rule_1 = new RuleDto('id1', creator, creator, creation, creation, version, true, {}, 'contentChanged', {}, 'webhook'); + const rule_2 = rule_1.update(update, modifier, newVersion, modified); + + expect(rule_2.trigger).toEqual(update.trigger); + expect(rule_2.triggerType).toEqual(update.trigger.triggerType); + expect(rule_2.action).toEqual(update.action); + expect(rule_2.actionType).toEqual(update.action.actionType); + expect(rule_2.lastModified).toEqual(modified); + expect(rule_2.lastModifiedBy).toEqual(modifier); + expect(rule_2.version).toEqual(newVersion); + }); + + it('should enable', () => { + const rule_1 = new RuleDto('id1', creator, creator, creation, creation, version, true, {}, 'contentChanged', {}, 'webhook'); + const rule_2 = rule_1.enable(modifier, newVersion, modified); + + expect(rule_2.isEnabled).toBeTruthy(); + expect(rule_2.lastModified).toEqual(modified); + expect(rule_2.lastModifiedBy).toEqual(modifier); + expect(rule_2.version).toEqual(newVersion); + }); + + it('should disable', () => { + const rule_1 = new RuleDto('id1', creator, creator, creation, creation, version, true, {}, 'contentChanged', {}, 'webhook'); + const rule_2 = rule_1.disable(modifier, newVersion, modified); + + expect(rule_2.isEnabled).toBeFalsy(); + expect(rule_2.lastModified).toEqual(modified); + expect(rule_2.lastModifiedBy).toEqual(modifier); + expect(rule_2.version).toEqual(newVersion); + }); +}); + +describe('RulesService', () => { + const now = DateTime.now(); + const user = 'me'; + const version = new Version('1'); + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule + ], + providers: [ + RulesService, + { provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }, + { provide: AnalyticsService, useValue: new AnalyticsService() } + ] + }); + }); + + afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => { + httpMock.verify(); + })); + + it('should make get request to get app rules', + inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { + + let rules: RuleDto[] | null = null; + + rulesService.getRules('my-app').subscribe(result => { + rules = result; + }); + + const req = httpMock.expectOne('http://service/p/api/apps/my-app/rules'); + + expect(req.request.method).toEqual('GET'); + expect(req.request.headers.get('If-Match')).toBeNull(); + + req.flush([ + { + id: 'id1', + created: '2016-12-12T10:10', + createdBy: 'CreatedBy1', + lastModified: '2017-12-12T10:10', + lastModifiedBy: 'LastModifiedBy1', + url: 'http://squidex.io/hook', + version: '1', + trigger: { + param1: 1, + param2: 2, + triggerType: 'ContentChanged' + }, + action: { + param3: 3, + param4: 4, + actionType: 'Webhook' + }, + isEnabled: true + } + ]); + + expect(rules).toEqual([ + new RuleDto('id1', 'CreatedBy1', 'LastModifiedBy1', + DateTime.parseISO_UTC('2016-12-12T10:10'), + DateTime.parseISO_UTC('2017-12-12T10:10'), + version, + true, + { + param1: 1, + param2: 2 + }, + 'ContentChanged', + { + param3: 3, + param4: 4 + }, + 'Webhook') + ]); + })); + + it('should make post request to create rule', + inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { + + const dto = new CreateRuleDto({ + param1: 1, + param2: 2, + triggerType: 'ContentChanged' + }, { + param3: 3, + param4: 4, + actionType: 'Webhook' + }); + + let rule: RuleDto | null = null; + + rulesService.postRule('my-app', dto, user, now).subscribe(result => { + rule = result; + }); + + const req = httpMock.expectOne('http://service/p/api/apps/my-app/rules'); + + expect(req.request.method).toEqual('POST'); + expect(req.request.headers.get('If-Match')).toBeNull(); + + req.flush({ id: 'id1', sharedSecret: 'token1', schemaId: 'schema1' }, { + headers: { + etag: '1' + } + }); + + expect(rule).toEqual( + new RuleDto('id1', 'CreatedBy1', 'LastModifiedBy1', + DateTime.parseISO_UTC('2016-12-12T10:10'), + DateTime.parseISO_UTC('2017-12-12T10:10'), + version, + true, + { + param1: 1, + param2: 2 + }, + 'ContentChanged', + { + param3: 3, + param4: 4 + }, + 'Webhook')); + })); + + it('should make put request to update rule', + inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { + + const dto = new UpdateRuleDto({ param1: 1 }, { param2: 2 }); + + rulesService.putRule('my-app', '123', dto, version).subscribe(); + + const req = httpMock.expectOne('http://service/p/api/apps/my-app/rules/123'); + + expect(req.request.method).toEqual('PUT'); + expect(req.request.headers.get('If-Match')).toEqual(version.value); + + req.flush({}); + })); + + it('should make put request to enable rule', + inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { + + rulesService.enableRule('my-app', '123', version).subscribe(); + + const req = httpMock.expectOne('http://service/p/api/apps/my-app/rules/123/enable'); + + expect(req.request.method).toEqual('PUT'); + expect(req.request.headers.get('If-Match')).toEqual(version.value); + + req.flush({}); + })); + + it('should make put request to disable rule', + inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { + + rulesService.disableRule('my-app', '123', version).subscribe(); + + const req = httpMock.expectOne('http://service/p/api/apps/my-app/rules/123/disable'); + + expect(req.request.method).toEqual('PUT'); + expect(req.request.headers.get('If-Match')).toEqual(version.value); + + req.flush({}); + })); + + it('should make delete request to delete rule', + inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { + + rulesService.deleteRule('my-app', '123', version).subscribe(); + + const req = httpMock.expectOne('http://service/p/api/apps/my-app/rules/123'); + + expect(req.request.method).toEqual('DELETE'); + expect(req.request.headers.get('If-Match')).toEqual(version.value); + })); + + it('should make get request to get app rule events', + inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { + + let rules: RuleEventsDto | null = null; + + rulesService.getEvents('my-app', 10, 20).subscribe(result => { + rules = result; + }); + + const req = httpMock.expectOne('http://service/p/api/apps/my-app/rules/events?take=10&skip=20'); + + expect(req.request.method).toEqual('GET'); + + req.flush({ + total: 20, + items: [ + { + id: 'id1', + created: '2017-12-12T10:10', + eventName: 'event1', + nextAttempt: '2017-12-12T12:10', + jobResult: 'Failed', + lastDump: 'dump1', + numCalls: 1, + description: 'url1', + result: 'Failed' + }, + { + id: 'id2', + created: '2017-12-13T10:10', + eventName: 'event2', + nextAttempt: '2017-12-13T12:10', + jobResult: 'Failed', + lastDump: 'dump2', + numCalls: 2, + description: 'url2', + result: 'Failed' + } + ] + }); + + expect(rules).toEqual( + new RuleEventsDto(20, [ + new RuleEventDto('id1', + DateTime.parseISO_UTC('2017-12-12T10:10'), + DateTime.parseISO_UTC('2017-12-12T12:10'), + 'event1', 'url1', 'dump1', 'Failed', 'Failed', 1), + new RuleEventDto('id2', + DateTime.parseISO_UTC('2017-12-13T10:10'), + DateTime.parseISO_UTC('2017-12-13T12:10'), + 'event2', 'url2', 'dump2', 'Failed', 'Failed', 2) + ])); + })); + + it('should make put request to enqueue rule event', + inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { + + rulesService.enqueueEvent('my-app', '123').subscribe(); + + const req = httpMock.expectOne('http://service/p/api/apps/my-app/rules/events/123'); + + expect(req.request.method).toEqual('PUT'); + })); +}); \ No newline at end of file diff --git a/src/Squidex/app/shared/services/rules.service.ts b/src/Squidex/app/shared/services/rules.service.ts new file mode 100644 index 000000000..8981dfbcd --- /dev/null +++ b/src/Squidex/app/shared/services/rules.service.ts @@ -0,0 +1,260 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; + +import 'framework/angular/http-extensions'; + +import { + AnalyticsService, + ApiUrlConfig, + DateTime, + HTTP, + Version, + Versioned +} from 'framework'; + +export const ruleTriggers: any = { + 'ContentChanged': 'Content changed' +}; + +export const ruleActions: any = { + 'Webhook': 'Send Webhooks' +}; + +export class RuleDto { + constructor( + public readonly id: string, + public readonly createdBy: string, + public readonly lastModifiedBy: string, + public readonly created: DateTime, + public readonly lastModified: DateTime, + public readonly version: Version, + public readonly isEnabled: boolean, + public readonly trigger: any, + public readonly triggerType: string, + public readonly action: any, + public readonly actionType: string + ) { + } + + public update(update: UpdateRuleDto, user: string, version: Version, now?: DateTime): RuleDto { + return new RuleDto( + this.id, + this.createdBy, user, + this.created, now || DateTime.now(), + version, + this.isEnabled, + update.trigger, + update.trigger['triggerType'], + update.action, + update.action['actionType']); + } + + public enable(user: string, version: Version, now?: DateTime): RuleDto { + return new RuleDto( + this.id, + this.createdBy, user, + this.created, now || DateTime.now(), + version, + true, + this.trigger, + this.triggerType, + this.action, + this.actionType); + } + + public disable(user: string, version: Version, now?: DateTime): RuleDto { + return new RuleDto( + this.id, + this.createdBy, user, + this.created, now || DateTime.now(), + version, + true, + this.trigger, + this.triggerType, + this.action, + this.actionType); + } +} + +export class RuleEventDto { + constructor( + public readonly id: string, + public readonly created: DateTime, + public readonly nextAttempt: DateTime | null, + public readonly eventName: string, + public readonly description: string, + public readonly lastDump: string, + public readonly result: string, + public readonly jobResult: string, + public readonly numCalls: number + ) { + } +} + +export class RuleEventsDto { + constructor( + public readonly total: number, + public readonly items: RuleEventDto[] + ) { + } +} + +export class CreateRuleDto { + constructor( + public readonly trigger: any, + public readonly action: any + ) { + } +} + +export class UpdateRuleDto { + constructor( + public readonly trigger: any, + public readonly action: any + ) { + } +} + +@Injectable() +export class RulesService { + constructor( + private readonly http: HttpClient, + private readonly apiUrl: ApiUrlConfig, + private readonly analytics: AnalyticsService + ) { + } + + public getRules(appName: string): Observable { + const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules`); + + return HTTP.getVersioned(this.http, url) + .map(response => { + const items: any[] = response.payload.body; + + return items.map(item => { + return new RuleDto( + item.id, + item.createdBy, + item.lastModifiedBy, + DateTime.parseISO_UTC(item.created), + DateTime.parseISO_UTC(item.lastModified), + new Version(item.version.toString()), + item.isEnabled, + item.trigger, + item.trigger.triggerType, + item.action, + item.action.actionType); + }); + }) + .pretifyError('Failed to load Rules. Please reload.'); + } + + public postRule(appName: string, dto: CreateRuleDto, user: string, now: DateTime): Observable { + const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules`); + + return HTTP.postVersioned(this.http, url, dto) + .map(response => { + const body = response.payload.body; + + return new RuleDto( + body.id, + user, + user, + now, + now, + response.version, + true, + dto.trigger, + dto.trigger.triggerType, + dto.action, + dto.action.actionType); + }) + .do(() => { + this.analytics.trackEvent('Rule', 'Created', appName); + }) + .pretifyError('Failed to create rule. Please reload.'); + } + + public putRule(appName: string, id: string, dto: UpdateRuleDto, version: Version): Observable> { + const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/${id}`); + + return HTTP.putVersioned(this.http, url, dto, version) + .do(() => { + this.analytics.trackEvent('Rule', 'Updated', appName); + }) + .pretifyError('Failed to update rule. Please reload.'); + } + + public enableRule(appName: string, id: string, version: Version): Observable> { + const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/${id}/enable`); + + return HTTP.putVersioned(this.http, url, {}, version) + .do(() => { + this.analytics.trackEvent('Rule', 'Updated', appName); + }) + .pretifyError('Failed to enable rule. Please reload.'); + } + + public disableRule(appName: string, id: string, version: Version): Observable> { + const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/${id}/disable`); + + return HTTP.putVersioned(this.http, url, {}, version) + .do(() => { + this.analytics.trackEvent('Rule', 'Updated', appName); + }) + .pretifyError('Failed to disable rule. Please reload.'); + } + + public deleteRule(appName: string, id: string, version: Version): Observable { + const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/${id}`); + + return HTTP.deleteVersioned(this.http, url, version) + .do(() => { + this.analytics.trackEvent('Rule', 'Deleted', appName); + }) + .pretifyError('Failed to delete rule. Please reload.'); + } + + public getEvents(appName: string, take: number, skip: number): Observable { + const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/events?take=${take}&skip=${skip}`); + + return HTTP.getVersioned(this.http, url) + .map(response => { + const body = response.payload.body; + + const items: any[] = body.items; + + return new RuleEventsDto(body.total, items.map(item => { + return new RuleEventDto( + item.id, + DateTime.parseISO_UTC(item.created), + item.nextAttempt ? DateTime.parseISO_UTC(item.nextAttempt) : null, + item.eventName, + item.description, + item.lastDump, + item.result, + item.jobResult, + item.numCalls); + })); + }) + .pretifyError('Failed to load events. Please reload.'); + } + + public enqueueEvent(appName: string, id: string): Observable { + const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/events/${id}`); + + return HTTP.putVersioned(this.http, url, {}) + .do(() => { + this.analytics.trackEvent('Rule', 'EventEnqueued', appName); + }) + .pretifyError('Failed to enqueue rule event. Please reload.'); + } +} \ No newline at end of file diff --git a/src/Squidex/app/shared/services/webhooks.service.spec.ts b/src/Squidex/app/shared/services/webhooks.service.spec.ts deleted file mode 100644 index d25b39c94..000000000 --- a/src/Squidex/app/shared/services/webhooks.service.spec.ts +++ /dev/null @@ -1,245 +0,0 @@ -/* - * Squidex Headless CMS - * - * @license - * Copyright (c) Sebastian Stehle. All rights reserved - */ - - -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { inject, TestBed } from '@angular/core/testing'; - -import { - AnalyticsService, - ApiUrlConfig, - CreateWebhookDto, - DateTime, - UpdateWebhookDto, - Version, - WebhookDto, - WebhookEventDto, - WebhookEventsDto, - WebhookSchemaDto, - WebhooksService -} from './../'; - -describe('WebhookDto', () => { - const creation = DateTime.today(); - const creator = 'not-me'; - const modified = DateTime.now(); - const modifier = 'me'; - const version = new Version('1'); - const newVersion = new Version('2'); - - it('should update url and schemas', () => { - const webhook_1 = new WebhookDto('id1', 'token1', creator, creator, creation, creation, version, [], 'http://squidex.io/hook', 1, 2, 3, 4); - const webhook_2 = - webhook_1.update(new UpdateWebhookDto('http://squidex.io/hook2', - [ - new WebhookSchemaDto('1', true, true, true, true), - new WebhookSchemaDto('2', true, true, true, true) - ]), modifier, newVersion, modified); - - expect(webhook_2.url).toEqual('http://squidex.io/hook2'); - expect(webhook_2.schemas.length).toEqual(2); - expect(webhook_2.lastModified).toEqual(modified); - expect(webhook_2.lastModifiedBy).toEqual(modifier); - expect(webhook_2.version).toEqual(newVersion); - }); -}); - -describe('WebhooksService', () => { - const now = DateTime.now(); - const user = 'me'; - const version = new Version('1'); - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - providers: [ - WebhooksService, - { provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }, - { provide: AnalyticsService, useValue: new AnalyticsService() } - ] - }); - }); - - afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => { - httpMock.verify(); - })); - - it('should make get request to get app webhooks', - inject([WebhooksService, HttpTestingController], (webhooksService: WebhooksService, httpMock: HttpTestingController) => { - - let webhooks: WebhookDto[] | null = null; - - webhooksService.getWebhooks('my-app').subscribe(result => { - webhooks = result; - }); - - const req = httpMock.expectOne('http://service/p/api/apps/my-app/webhooks'); - - expect(req.request.method).toEqual('GET'); - expect(req.request.headers.get('If-Match')).toBeNull(); - - req.flush([ - { - id: 'id1', - sharedSecret: 'token1', - created: '2016-12-12T10:10', - createdBy: 'CreatedBy1', - lastModified: '2017-12-12T10:10', - lastModifiedBy: 'LastModifiedBy1', - url: 'http://squidex.io/hook', - version: '1', - totalSucceeded: 1, - totalFailed: 2, - totalTimedout: 3, - averageRequestTimeMs: 4, - schemas: [{ - schemaId: '1', - sendCreate: true, - sendUpdate: true, - sendDelete: true, - sendPublish: true - }, { - schemaId: '2', - sendCreate: true, - sendUpdate: true, - sendDelete: true, - sendPublish: true - }] - } - ]); - - expect(webhooks).toEqual([ - new WebhookDto('id1', 'token1', 'CreatedBy1', 'LastModifiedBy1', - DateTime.parseISO_UTC('2016-12-12T10:10'), - DateTime.parseISO_UTC('2017-12-12T10:10'), - version, - [ - new WebhookSchemaDto('1', true, true, true, true), - new WebhookSchemaDto('2', true, true, true, true) - ], - 'http://squidex.io/hook', 1, 2, 3, 4) - ]); - })); - - it('should make post request to create webhook', - inject([WebhooksService, HttpTestingController], (webhooksService: WebhooksService, httpMock: HttpTestingController) => { - - const dto = new CreateWebhookDto('http://squidex.io/hook', []); - - let webhook: WebhookDto | null = null; - - webhooksService.postWebhook('my-app', dto, user, now).subscribe(result => { - webhook = result; - }); - - const req = httpMock.expectOne('http://service/p/api/apps/my-app/webhooks'); - - expect(req.request.method).toEqual('POST'); - expect(req.request.headers.get('If-Match')).toBeNull(); - - req.flush({ id: 'id1', sharedSecret: 'token1', schemaId: 'schema1' }, { - headers: { - etag: '1' - } - }); - - expect(webhook).toEqual( - new WebhookDto('id1', 'token1', user, user, now, now, version, [], 'http://squidex.io/hook', 0, 0, 0, 0)); - })); - - it('should make put request to update webhook', - inject([WebhooksService, HttpTestingController], (webhooksService: WebhooksService, httpMock: HttpTestingController) => { - - const dto = new UpdateWebhookDto('http://squidex.io/hook', []); - - webhooksService.putWebhook('my-app', '123', dto, version).subscribe(); - - const req = httpMock.expectOne('http://service/p/api/apps/my-app/webhooks/123'); - - expect(req.request.method).toEqual('PUT'); - expect(req.request.headers.get('If-Match')).toEqual(version.value); - - req.flush({}); - })); - - it('should make delete request to delete webhook', - inject([WebhooksService, HttpTestingController], (webhooksService: WebhooksService, httpMock: HttpTestingController) => { - - webhooksService.deleteWebhook('my-app', '123', version).subscribe(); - - const req = httpMock.expectOne('http://service/p/api/apps/my-app/webhooks/123'); - - expect(req.request.method).toEqual('DELETE'); - expect(req.request.headers.get('If-Match')).toEqual(version.value); - })); - - it('should make get request to get app webhook events', - inject([WebhooksService, HttpTestingController], (webhooksService: WebhooksService, httpMock: HttpTestingController) => { - - let webhooks: WebhookEventsDto | null = null; - - webhooksService.getEvents('my-app', 10, 20).subscribe(result => { - webhooks = result; - }); - - const req = httpMock.expectOne('http://service/p/api/apps/my-app/webhooks/events?take=10&skip=20'); - - expect(req.request.method).toEqual('GET'); - - req.flush({ - total: 20, - items: [ - { - id: 'id1', - created: '2017-12-12T10:10', - eventName: 'event1', - nextAttempt: '2017-12-12T12:10', - jobResult: 'Failed', - lastDump: 'dump1', - numCalls: 1, - requestUrl: 'url1', - result: 'Failed' - }, - { - id: 'id2', - created: '2017-12-13T10:10', - eventName: 'event2', - nextAttempt: '2017-12-13T12:10', - jobResult: 'Failed', - lastDump: 'dump2', - numCalls: 2, - requestUrl: 'url2', - result: 'Failed' - } - ] - }); - - expect(webhooks).toEqual( - new WebhookEventsDto(20, [ - new WebhookEventDto('id1', - DateTime.parseISO_UTC('2017-12-12T10:10'), - DateTime.parseISO_UTC('2017-12-12T12:10'), - 'event1', 'url1', 'dump1', 'Failed', 'Failed', 1), - new WebhookEventDto('id2', - DateTime.parseISO_UTC('2017-12-13T10:10'), - DateTime.parseISO_UTC('2017-12-13T12:10'), - 'event2', 'url2', 'dump2', 'Failed', 'Failed', 2) - ])); - })); - - it('should make put request to enqueue webhook event', - inject([WebhooksService, HttpTestingController], (webhooksService: WebhooksService, httpMock: HttpTestingController) => { - - webhooksService.enqueueEvent('my-app', '123').subscribe(); - - const req = httpMock.expectOne('http://service/p/api/apps/my-app/webhooks/events/123'); - - expect(req.request.method).toEqual('PUT'); - })); -}); \ No newline at end of file diff --git a/src/Squidex/app/shared/services/webhooks.service.ts b/src/Squidex/app/shared/services/webhooks.service.ts deleted file mode 100644 index 596409cf7..000000000 --- a/src/Squidex/app/shared/services/webhooks.service.ts +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Squidex Headless CMS - * - * @license - * Copyright (c) Sebastian Stehle. All rights reserved - */ - -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { Observable } from 'rxjs'; - -import 'framework/angular/http-extensions'; - -import { - AnalyticsService, - ApiUrlConfig, - DateTime, - HTTP, - Version, - Versioned -} from 'framework'; - -export class WebhookDto { - constructor( - public readonly id: string, - public readonly sharedSecret: string, - public readonly createdBy: string, - public readonly lastModifiedBy: string, - public readonly created: DateTime, - public readonly lastModified: DateTime, - public readonly version: Version, - public readonly schemas: WebhookSchemaDto[], - public readonly url: string, - public readonly totalSucceeded: number, - public readonly totalFailed: number, - public readonly totalTimedout: number, - public readonly averageRequestTimeMs: number - ) { - } - - public update(update: UpdateWebhookDto, user: string, version: Version, now?: DateTime): WebhookDto { - return new WebhookDto( - this.id, - this.sharedSecret, - this.createdBy, user, - this.created, now || DateTime.now(), - version, - update.schemas, - update.url, - this.totalSucceeded, - this.totalFailed, - this.totalTimedout, - this.averageRequestTimeMs); - } -} - -export class WebhookSchemaDto { - constructor( - public readonly schemaId: string, - public readonly sendCreate: boolean, - public readonly sendUpdate: boolean, - public readonly sendDelete: boolean, - public readonly sendPublish: boolean - ) { - } -} - -export class WebhookEventDto { - constructor( - public readonly id: string, - public readonly created: DateTime, - public readonly nextAttempt: DateTime | null, - public readonly eventName: string, - public readonly requestUrl: string, - public readonly lastDump: string, - public readonly result: string, - public readonly jobResult: string, - public readonly numCalls: number - ) { - } -} - -export class WebhookEventsDto { - constructor( - public readonly total: number, - public readonly items: WebhookEventDto[] - ) { - } -} - -export class CreateWebhookDto { - constructor( - public readonly url: string, - public readonly schemas: WebhookSchemaDto[] - ) { - } -} - -export class UpdateWebhookDto { - constructor( - public readonly url: string, - public readonly schemas: WebhookSchemaDto[] - ) { - } -} - -@Injectable() -export class WebhooksService { - constructor( - private readonly http: HttpClient, - private readonly apiUrl: ApiUrlConfig, - private readonly analytics: AnalyticsService - ) { - } - - public getWebhooks(appName: string): Observable { - const url = this.apiUrl.buildUrl(`api/apps/${appName}/webhooks`); - - return HTTP.getVersioned(this.http, url) - .map(response => { - const items: any[] = response.payload.body; - - return items.map(item => { - const schemas = item.schemas.map((schema: any) => - new WebhookSchemaDto( - schema.schemaId, - schema.sendCreate, - schema.sendUpdate, - schema.sendDelete, - schema.sendPublish)); - - return new WebhookDto( - item.id, - item.sharedSecret, - item.createdBy, - item.lastModifiedBy, - DateTime.parseISO_UTC(item.created), - DateTime.parseISO_UTC(item.lastModified), - new Version(item.version.toString()), - schemas, - item.url, - item.totalSucceeded, - item.totalFailed, - item.totalTimedout, - item.averageRequestTimeMs); - }); - }) - .pretifyError('Failed to load webhooks. Please reload.'); - } - - public postWebhook(appName: string, dto: CreateWebhookDto, user: string, now: DateTime): Observable { - const url = this.apiUrl.buildUrl(`api/apps/${appName}/webhooks`); - - return HTTP.postVersioned(this.http, url, dto) - .map(response => { - const body = response.payload.body; - - return new WebhookDto( - body.id, - body.sharedSecret, - user, - user, - now, - now, - response.version, - dto.schemas, - dto.url, - 0, 0, 0, 0); - }) - .do(() => { - this.analytics.trackEvent('Webhook', 'Created', appName); - }) - .pretifyError('Failed to create webhook. Please reload.'); - } - - public putWebhook(appName: string, id: string, dto: UpdateWebhookDto, version: Version): Observable> { - const url = this.apiUrl.buildUrl(`api/apps/${appName}/webhooks/${id}`); - - return HTTP.putVersioned(this.http, url, dto, version) - .do(() => { - this.analytics.trackEvent('Webhook', 'Updated', appName); - }) - .pretifyError('Failed to update webhook. Please reload.'); - } - - public deleteWebhook(appName: string, id: string, version: Version): Observable { - const url = this.apiUrl.buildUrl(`api/apps/${appName}/webhooks/${id}`); - - return HTTP.deleteVersioned(this.http, url, version) - .do(() => { - this.analytics.trackEvent('Webhook', 'Deleted', appName); - }) - .pretifyError('Failed to delete webhook. Please reload.'); - } - - public getEvents(appName: string, take: number, skip: number): Observable { - const url = this.apiUrl.buildUrl(`api/apps/${appName}/webhooks/events?take=${take}&skip=${skip}`); - - return HTTP.getVersioned(this.http, url) - .map(response => { - const body = response.payload.body; - - const items: any[] = body.items; - - return new WebhookEventsDto(body.total, items.map(item => { - return new WebhookEventDto( - item.id, - DateTime.parseISO_UTC(item.created), - item.nextAttempt ? DateTime.parseISO_UTC(item.nextAttempt) : null, - item.eventName, - item.requestUrl, - item.lastDump, - item.result, - item.jobResult, - item.numCalls); - })); - }) - .pretifyError('Failed to load events. Please reload.'); - } - - public enqueueEvent(appName: string, id: string): Observable { - const url = this.apiUrl.buildUrl(`api/apps/${appName}/webhooks/events/${id}`); - - return HTTP.putVersioned(this.http, url, {}) - .do(() => { - this.analytics.trackEvent('Webhook', 'EventEnqueued', appName); - }) - .pretifyError('Failed to enqueue webhook event. Please reload.'); - } -} \ No newline at end of file diff --git a/src/Squidex/app/shell/pages/app/left-menu.component.html b/src/Squidex/app/shell/pages/app/left-menu.component.html index f420ad2a4..70a4b5359 100644 --- a/src/Squidex/app/shell/pages/app/left-menu.component.html +++ b/src/Squidex/app/shell/pages/app/left-menu.component.html @@ -16,8 +16,8 @@