Browse Source

Started with UI

pull/157/head
Sebastian Stehle 9 years ago
parent
commit
486da51899
  1. 2
      src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchema.cs
  2. 2
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs
  3. 2
      src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository_EventHandling.cs
  4. 6
      src/Squidex.Domain.Apps.Write/Rules/Commands/CreateRule.cs
  5. 2
      src/Squidex/Controllers/Api/Rules/Models/Triggers/ContentChangedTriggerSchemaDto.cs
  6. 4
      src/Squidex/app/app.routes.ts
  7. 3
      src/Squidex/app/features/apps/pages/apps-page.component.html
  8. 13
      src/Squidex/app/features/rules/declarations.ts
  9. 0
      src/Squidex/app/features/rules/index.ts
  10. 30
      src/Squidex/app/features/rules/module.ts
  11. 4
      src/Squidex/app/features/rules/pages/events/rule-events-page.component.html
  12. 0
      src/Squidex/app/features/rules/pages/events/rule-events-page.component.scss
  13. 24
      src/Squidex/app/features/rules/pages/events/rule-events-page.component.ts
  14. 29
      src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.html
  15. 2
      src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.scss
  16. 55
      src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.ts
  17. 79
      src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html
  18. 38
      src/Squidex/app/features/rules/pages/rules/rule-wizard.component.scss
  19. 96
      src/Squidex/app/features/rules/pages/rules/rule-wizard.component.ts
  20. 51
      src/Squidex/app/features/rules/pages/rules/rules-page.component.html
  21. 0
      src/Squidex/app/features/rules/pages/rules/rules-page.component.scss
  22. 66
      src/Squidex/app/features/rules/pages/rules/rules-page.component.ts
  23. 71
      src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html
  24. 8
      src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.scss
  25. 147
      src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts
  26. 5
      src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html
  27. 10
      src/Squidex/app/features/webhooks/declarations.ts
  28. 149
      src/Squidex/app/features/webhooks/pages/webhook.component.html
  29. 13
      src/Squidex/app/features/webhooks/pages/webhook.component.scss
  30. 174
      src/Squidex/app/features/webhooks/pages/webhook.component.ts
  31. 72
      src/Squidex/app/features/webhooks/pages/webhooks-page.component.html
  32. 136
      src/Squidex/app/features/webhooks/pages/webhooks-page.component.ts
  33. 26
      src/Squidex/app/framework/angular/keys.pipe.spec.ts
  34. 18
      src/Squidex/app/framework/angular/keys.pipe.ts
  35. 1
      src/Squidex/app/framework/declarations.ts
  36. 3
      src/Squidex/app/framework/module.ts
  37. 2
      src/Squidex/app/shared/declarations-base.ts
  38. 6
      src/Squidex/app/shared/module.ts
  39. 310
      src/Squidex/app/shared/services/rules.service.spec.ts
  40. 260
      src/Squidex/app/shared/services/rules.service.ts
  41. 245
      src/Squidex/app/shared/services/webhooks.service.spec.ts
  42. 230
      src/Squidex/app/shared/services/webhooks.service.ts
  43. 4
      src/Squidex/app/shell/pages/app/left-menu.component.html
  44. 5
      src/Squidex/app/shell/pages/internal/apps-menu.component.html
  45. 6
      src/Squidex/app/theme/_mixins.scss
  46. 56
      src/Squidex/app/theme/_rules.scss
  47. 48
      src/Squidex/app/theme/icomoon/demo.html
  48. BIN
      src/Squidex/app/theme/icomoon/fonts/icomoon.eot
  49. 4
      src/Squidex/app/theme/icomoon/fonts/icomoon.svg
  50. BIN
      src/Squidex/app/theme/icomoon/fonts/icomoon.ttf
  51. BIN
      src/Squidex/app/theme/icomoon/fonts/icomoon.woff
  52. 4
      src/Squidex/app/theme/icomoon/selection.json
  53. 19
      src/Squidex/app/theme/icomoon/style.css
  54. 1
      src/Squidex/app/theme/theme.scss

2
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; }
}
}

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

2
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<IEvent> @event)

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

2
src/Squidex/Controllers/Api/Rules/Models/Triggers/ContentChangedTriggerSchemaDto.cs

@ -35,6 +35,6 @@ namespace Squidex.Controllers.Api.Rules.Models.Triggers
/// <summary>
/// True, when to send a message for published events.
/// </summary>
public bool SendPublish { get; set; }
public bool SendChange { get; set; }
}
}

4
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',

3
src/Squidex/app/features/apps/pages/apps-page.component.html

@ -31,7 +31,8 @@
<div class="modal-body">
<sqx-app-form
(created)="addAppDialog.hide()"
(cancelled)="addAppDialog.hide()"></sqx-app-form>
(cancelled)="addAppDialog.hide()">
</sqx-app-form>
</div>
</div>
</div>

13
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';

0
src/Squidex/app/features/webhooks/index.ts → src/Squidex/app/features/rules/index.ts

30
src/Squidex/app/features/webhooks/module.ts → 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 { }
export class SqxFeatureRulesModule { }

4
src/Squidex/app/features/webhooks/pages/webhook-events-page.component.html → src/Squidex/app/features/rules/pages/events/rule-events-page.component.html

@ -1,4 +1,4 @@
<sqx-title message="{app} | Webhooks Events" parameter1="app" value1="{{appName() | async}}"></sqx-title>
<sqx-title message="{app} | Rules Events" parameter1="app" value1="{{appName() | async}}"></sqx-title>
<sqx-panel panelWidth="63rem">
<div class="panel-header">
@ -57,7 +57,7 @@
<span class="truncate">{{event.eventName}}</span>
</td>
<td>
<span class="truncate">{{event.requestUrl}}</span>
<span class="truncate">{{event.description}}</span>
</td>
<td>
<small class="item-modified">{{event.created | sqxFromNow}}</small>

0
src/Squidex/app/features/webhooks/pages/webhook-events-page.component.scss → src/Squidex/app/features/rules/pages/events/rule-events-page.component.scss

24
src/Squidex/app/features/webhooks/pages/webhook-events-page.component.ts → 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<WebhookEventDto>();
export class RuleEventsPageComponent extends AppComponentBase implements OnInit {
public eventsItems = ImmutableArray.empty<RuleEventDto>();
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);
});

29
src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.html

@ -0,0 +1,29 @@
<form [formGroup]="actionForm" class="form-horizontal" (ngSubmit)="save()">
<div class="form-group row">
<label class="col col-2 col-form-label" for="url">Url</label>
<div class="col col-10">
<sqx-control-errors for="url" [submitted]="actionFormSubmitted"></sqx-control-errors>
<input type="url" class="form-control" id="url" formControlName="url" />
<small class="form-text text-muted">
The url where the events will be sent to.
</small>
</div>
</div>
<div class="form-group row">
<label class="col col-2 col-form-label" for="sharedSecret">Secret</label>
<div class="col col-10">
<sqx-control-errors for="sharedSecret" [submitted]="actionFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" id="sharedSecret" formControlName="sharedSecret" />
<small class="form-text text-muted">
The shared secret will be used to add a header X-Signature=Sha256(RequestBody + Secret)
</small>
</div>
</div>
</form>

2
src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.scss

@ -0,0 +1,2 @@
@import '_vars';
@import '_mixins';

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

79
src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html

@ -0,0 +1,79 @@
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" *ngIf="step === 1">
Step 1 of 4: Select Trigger
</h4>
<h4 class="modal-title" *ngIf="step === 2">
Step 2 of 4: Configure Trigger
</h4>
<h4 class="modal-title" *ngIf="step === 3">
Step 3 of 4: Select Action
</h4>
<h4 class="modal-title" *ngIf="step === 4">
Step 4 of 4: Configure Action
</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" (click)="cancel()">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div *ngIf="step === 1">
<span *ngFor="let trigger of ruleTriggers | sqxKeys" class="rule-element rule-element-{{trigger}}" (click)="selectTriggerType(trigger)">
<span class="rule-element-icon">
<i class="icon-trigger-{{trigger}}"></i>
</span>
<span class="rule-element-text">
{{ruleTriggers[trigger]}}
</span>
</span>
</div>
<div *ngIf="step === 2 && schemas" class="modal-form">
<div [ngSwitch]="triggerType">
<div *ngSwitchCase="'ContentChanged'">
<sqx-content-changed-trigger #triggerControl
[schemas]="schemas"
[trigger]="trigger"
(triggerChanged)="selectTrigger($event)">
</sqx-content-changed-trigger>
</div>
</div>
</div>
<div *ngIf="step === 3">
<span *ngFor="let action of ruleActions | sqxKeys" class="rule-element rule-element-{{action}}" (click)="selectActionType(action)">
<span class="rule-element-icon">
<i class="icon-action-{{action}}"></i>
</span>
<span class="rule-element-text">
{{ruleActions[action]}}
</span>
</span>
</div>
<div *ngIf="step === 4" class="modal-form">
<div [ngSwitch]="actionType">
<div *ngSwitchCase="'Webhook'">
<sqx-webhook-action #actionControl
[action]="action"
(actionChanged)="selectAction($event)">
</sqx-webhook-action>
</div>
</div>
</div>
</div>
<div class="modal-footer" *ngIf="step === 2 || step === 4">
<div class="clearfix" *ngIf="step === 2">
<button type="reset" class="float-left btn btn-secondary" (click)="cancel()">Cancel</button>
<button type="submit" class="float-right btn btn-primary" (click)="triggerControl.save()">Next</button>
</div>
<div class="clearfix" *ngIf="step === 4">
<button type="reset" class="float-left btn btn-secondary" (click)="cancel()">Cancel</button>
<button type="submit" class="float-right btn btn-primary" (click)="actionControl.save()">Save</button>
</div>
</div>
</div>
</div>

38
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;
}

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

51
src/Squidex/app/features/rules/pages/rules/rules-page.component.html

@ -0,0 +1,51 @@
<sqx-title message="{app} | Rules" parameter1="app" value1="{{appName() | async}}"></sqx-title>
<sqx-panel panelWidth="50rem">
<div class="panel-header">
<div class="panel-title-row">
<div class="float-right">
<button class="btn btn-link btn-decent" (click)="load(true)" title="Refresh Assets (CTRL + SHIFT + R)">
<i class="icon-reset"></i> Refresh
</button>
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut>
<sqx-shortcut keys="ctrl+shift+g" (trigger)="buttonNew.click()"></sqx-shortcut>
<button class="btn btn-success" #buttonNew (click)="addRuleDialog.show()" title="New Rule (CTRL + M)">
<i class="icon-plus"></i> New
</button>
</div>
<h3 class="panel-title">Rules</h3>
</div>
<a class="panel-close" sqxParentLink isLazyLoaded="true">
<i class="icon-close"></i>
</a>
</div>
<div class="panel-main">
<div class="panel-content panel-content-scroll">
<div class="table-items-row table-items-row-empty" *ngIf="rules && rules.length === 0">
No Rule created yet.
</div>
</div>
</div>
<div class="panel-sidebar">
<a class="panel-link" routerLink="events" routerLinkActive="active" #linkHistory>
<i class="icon-time"></i>
</a>
</div>
</sqx-panel>
<div class="modal" *sqxModalView="addRuleDialog;onRoot:true;closeAuto:false" @fade>
<div class="modal-backdrop"></div>
<sqx-rule-wizard [schemas]="schemas"
(cancelled)="addRuleDialog.hide()">
</sqx-rule-wizard>
</div>
<router-outlet></router-outlet>

0
src/Squidex/app/features/webhooks/pages/webhooks-page.component.scss → src/Squidex/app/features/rules/pages/rules/rules-page.component.scss

66
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<RuleDto>;
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);
});
}
}

71
src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html

@ -0,0 +1,71 @@
<table class="table table-middle table-sm table-fixed table-borderless">
<colgroup>
<col style="width: 100%" />
<col style="width: 40px" />
<col style="width: 40px" />
<col style="width: 40px" />
<col style="width: 40px" />
<col style="width: 40px" />
<col style="width: 40px" />
</colgroup>
<tr>
<th>
Schema
</th>
<th class="text-center" title="All">
All
</th>
<th class="text-center" title="Created">
C
</th>
<th class="text-center" title="Updated">
U
</th>
<th class="text-center" title="Deleted">
D
</th>
<th class="text-center" title="Changed">
C
</th>
<th></th>
</tr>
<tr *ngFor="let schema of triggerSchemas">
<td>
<span class="truncate">{{schema.schema.name}}</span>
</td>
<td class="text-center" title="Created">
<input type="checkbox" [ngModel]="schema.sendAll" (ngModelChange)="toggleAll(schema)" />
</td>
<td class="text-center" title="Created">
<input type="checkbox" [ngModel]="schema.sendCreate" (ngModelChange)="toggle(schema, 'sendCreate')" />
</td>
<td class="text-center" title="Updated">
<input type="checkbox" [ngModel]="schema.sendUpdate" (ngModelChange)="toggle(schema, 'sendUpdate')" />
</td>
<td class="text-center" title="Deleted">
<input type="checkbox" [ngModel]="schema.sendDelete" (ngModelChange)="toggle(schema, 'sendDelete')" />
</td>
<td class="text-center" title="Published">
<input type="checkbox" [ngModel]="schema.sendChange" (ngModelChange)="toggle(schema, 'sendChange')" />
</td>
<td class="text-center">
<button type="button" class="btn btn-link btn-secondary" (click)="removeSchema(schema)">
<i class="icon-close"></i>
</button>
</td>
</tr>
</table>
<div class="section" *ngIf="schemasToAdd.length > 0">
<form class="form-inline" (ngSubmit)="addSchema()">
<div class="form-group mr-1">
<select class="form-control schemas-control" [(ngModel)]="schemaToAdd" name="schema">
<option *ngFor="let schema of schemasToAdd" [ngValue]="schema">{{schema.name}}</option>
</select>
</div>
<button type="submit" class="btn btn-success" [disabled]="!hasSchema">Add Schema</button>
</form>
</div>

8
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;
}

147
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<object>();
public triggerSchemas: ImmutableArray<TriggerSchemaForm>;
public schemaToAdd: SchemaDto;
public schemasToAdd: ImmutableArray<SchemaDto>;
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(<any>{ 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;
}
}

5
src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html

@ -66,7 +66,10 @@
</div>
<div class="modal-body">
<sqx-schema-form [appName]="appName() | async" (created)="onSchemaCreated($event)" (cancelled)="addSchemaDialog.hide()"></sqx-schema-form>
<sqx-schema-form [appName]="appName() | async"
(created)="onSchemaCreated($event)"
(cancelled)="addSchemaDialog.hide()">
</sqx-schema-form>
</div>
</div>
</div>

10
src/Squidex/app/features/webhooks/declarations.ts

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

149
src/Squidex/app/features/webhooks/pages/webhook.component.html

@ -1,149 +0,0 @@
<div class="table-items-row">
<form [formGroup]="webhookForm" (ngSubmit)="save()">
<table class="table table-middle table-sm table-borderless table-fixed">
<colgroup>
<col style="width: 120px; text-align: right;" />
<col style="width: 100%" />
<col style="width: 40px" />
</colgroup>
<tr>
<td class="text-right" colspan="2">
<button type="submit" class="btn btn-primary" [disabled]="!hasUrl">Save</button>
</td>
<td class="client-delete">
<button type="button" class="btn btn-link btn-danger"
(sqxConfirmClick)="deleting.emit()"
confirmTitle="Delete webhook"
confirmText="Do you really want to delete the webhook?">
<i class="icon-bin2"></i>
</button>
</td>
</tr>
<tr>
<td>Url:</td>
<td>
<input type="url" class="form-control" formControlName="url" [attr.value]="webhook.url" #inputUrl />
</td>
<td>
<button type="button" class="btn btn-primary btn-link" [sqxCopy]="inputUrl">
<i class="icon-copy"></i>
</button>
</td>
</tr>
<tr>
<td>Secret:</td>
<td>
<input readonly class="form-control" [attr.value]="webhook.sharedSecret" #inputSecret />
</td>
<td>
<button type="button" class="btn btn-primary btn-link" [sqxCopy]="inputSecret">
<i class="icon-copy"></i>
</button>
</td>
</tr>
</table>
</form>
<div class="webhook-section" *ngIf="schemas.length > 0">
<table class="table table-middle table-sm table-fixed table-borderless">
<colgroup>
<col style="width: 100%" />
<col style="width: 40px" />
<col style="width: 40px" />
<col style="width: 40px" />
<col style="width: 40px" />
<col style="width: 40px" />
<col style="width: 40px" />
</colgroup>
<tr>
<th>
Schema
</th>
<th class="text-center" title="All">
All
</th>
<th class="text-center" title="Created">
C
</th>
<th class="text-center" title="Updated">
U
</th>
<th class="text-center" title="Deleted">
D
</th>
<th class="text-center" title="Published">
P
</th>
<th></th>
</tr>
<tr *ngFor="let schema of schemas">
<td>
<span class="truncate">{{schema.schema.name}}</span>
</td>
<td class="text-center" title="Created">
<input type="checkbox" [ngModel]="schema.sendAll" (ngModelChange)="toggleAll(schema)" />
</td>
<td class="text-center" title="Created">
<input type="checkbox" [ngModel]="schema.sendCreate" (ngModelChange)="toggle(schema, 'sendCreate')" />
</td>
<td class="text-center" title="Updated">
<input type="checkbox" [ngModel]="schema.sendUpdate" (ngModelChange)="toggle(schema, 'sendUpdate')" />
</td>
<td class="text-center" title="Deleted">
<input type="checkbox" [ngModel]="schema.sendDelete" (ngModelChange)="toggle(schema, 'sendDelete')" />
</td>
<td class="text-center" title="Published">
<input type="checkbox" [ngModel]="schema.sendPublish" (ngModelChange)="toggle(schema, 'sendPublish')" />
</td>
<td class="text-center">
<button type="button" class="btn btn-link btn-secondary"
(sqxConfirmClick)="removeSchema(schema)"
confirmTitle="Remove schema"
confirmText="Do you really want to remove the schema?">
<i class="icon-close"></i>
</button>
</td>
</tr>
</table>
</div>
<div class="webhook-section" *ngIf="schemasToAdd.length > 0">
<form class="form-inline" (ngSubmit)="addSchema()">
<div class="form-group mr-1">
<select class="form-control schemas-control" [(ngModel)]="schemaToAdd" name="schema">
<option *ngFor="let schema of schemasToAdd" [ngValue]="schema">{{schema.name}}</option>
</select>
</div>
<button type="submit" class="btn btn-success" [disabled]="!hasSchema">Add Schema</button>
</form>
</div>
<div class="webhook-section" *ngIf="webhook.averageRequestTimeMs > 0">
<div class="row">
<div class="col-3">
<span title="Succeeded Requests" [class.success]="webhook.totalSucceeded > 0">
<i class="icon-checkmark"></i> {{webhook.totalSucceeded}}
</span>
</div>
<div class="col-3">
<span title="Failed Requests" [class.failed]="webhook.totalFailed > 0">
<i class="icon-bug"></i> {{webhook.totalFailed}}
</span>
</div>
<div class="col-3">
<span title="Timeout Requests" [class.failed]="webhook.totalTimedout > 0">
<i class="icon-timeout"></i> {{webhook.totalTimedout}}
</span>
</div>
<div class="col-3">
<span title="Average Response Time">
<i class="icon-elapsed"></i> {{webhook.averageRequestTimeMs}} ms
</span>
</div>
</div>
</div>
</div>

13
src/Squidex/app/features/webhooks/pages/webhook.component.scss

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

174
src/Squidex/app/features/webhooks/pages/webhook.component.ts

@ -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<UpdateWebhookDto>();
@Input()
public allSchemas: SchemaDto[];
@Input()
public webhook: WebhookDto;
public schemas: ImmutableArray<WebhookSchemaForm>;
public schemaToAdd: SchemaDto;
public schemasToAdd: ImmutableArray<SchemaDto>;
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(<any>{ 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;
}
}

72
src/Squidex/app/features/webhooks/pages/webhooks-page.component.html

@ -1,72 +0,0 @@
<sqx-title message="{app} | Webhooks" parameter1="app" value1="{{appName() | async}}"></sqx-title>
<sqx-panel panelWidth="50rem">
<div class="panel-header">
<div class="panel-title-row">
<div class="float-right">
<button class="btn btn-link btn-decent" (click)="load(true)" title="Refresh Assets (CTRL + SHIFT + R)">
<i class="icon-reset"></i> Refresh
</button>
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut>
</div>
<h3 class="panel-title">Webhooks</h3>
</div>
<a class="panel-close" sqxParentLink isLazyLoaded="true">
<i class="icon-close"></i>
</a>
</div>
<div class="panel-main">
<div class="panel-content panel-content-scroll">
<div class="table-items-row table-items-row-empty" *ngIf="webhooks && webhooks.length === 0">
No Webhook created yet.
</div>
<div *ngIf="webhooks">
<sqx-webhook *ngFor="let webhook of webhooks" [webhook]="webhook" [allSchemas]="schemas"
(updating)="updateWebhook(webhook, $event)"
(deleting)="deleteWebhook(webhook)"></sqx-webhook>
<div class="table-items-footer">
<form [formGroup]="addWebhookForm" (ngSubmit)="addWebhook()">
<div class="row no-gutters">
<div class="col">
<sqx-control-errors for="url" [submitted]="addWebhookFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control url-control" formControlName="url" placeholder="Enter webhook url" autocomplete="off" />
</div>
<div class="col col-auto pl-1">
<button type="submit" class="btn btn-success" [disabled]="!hasUrl">Add Webhook</button>
</div>
<div class="col col-auto pl-1">
<button type="reset" class="btn btn-secondary" (click)="cancelAddWebhook()" [disabled]="addWebhookFormSubmitted">Cancel</button>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="panel-sidebar">
<a class="panel-link" routerLink="events" routerLinkActive="active" #linkHistory>
<i class="icon-time"></i>
</a>
<a class="panel-link" routerLink="help" routerLinkActive="active" #linkHelp>
<i class="icon-help"></i>
</a>
<sqx-onboarding-tooltip id="history" [for]="linkHistory" position="leftTop" after="120000">
The sidebar navigation contains useful context specific links. Here you can view the history how this schema has changed over time.
</sqx-onboarding-tooltip>
<sqx-onboarding-tooltip id="help" [for]="linkHelp" position="leftTop" after="180000">
Click the help icon to show a context specific help page. Go to <a href="https://docs.squidex.io" target="_blank">https://docs.squidex.io</a> for the full documentation.
</sqx-onboarding-tooltip>
</div>
</div>
</sqx-panel>
<router-outlet></router-outlet>

136
src/Squidex/app/features/webhooks/pages/webhooks-page.component.ts

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

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

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

1
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';

3
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,

2
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';

6
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,

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

260
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<RuleDto[]> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules`);
return HTTP.getVersioned<any>(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<RuleDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules`);
return HTTP.postVersioned<any>(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<Versioned<any>> {
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<Versioned<any>> {
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<Versioned<any>> {
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<any> {
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<RuleEventsDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/events?take=${take}&skip=${skip}`);
return HTTP.getVersioned<any>(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<any> {
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.');
}
}

245
src/Squidex/app/shared/services/webhooks.service.spec.ts

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

230
src/Squidex/app/shared/services/webhooks.service.ts

@ -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<WebhookDto[]> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/webhooks`);
return HTTP.getVersioned<any>(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<WebhookDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/webhooks`);
return HTTP.postVersioned<any>(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<Versioned<any>> {
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<any> {
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<WebhookEventsDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/webhooks/events?take=${take}&skip=${skip}`);
return HTTP.getVersioned<any>(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<any> {
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.');
}
}

4
src/Squidex/app/shell/pages/app/left-menu.component.html

@ -16,8 +16,8 @@
</a>
</li>
<li class="nav-item" *ngIf="permission !== 'Editor'">
<a class="nav-link" routerLink="webhooks" routerLinkActive="active">
<i class="nav-icon icon-webhooks"></i> <div class="nav-text">Webhooks</div>
<a class="nav-link" routerLink="rules" routerLinkActive="active">
<i class="nav-icon icon-rules"></i> <div class="nav-text">Rules</div>
</a>
</li>
<li class="nav-item" *ngIf="permission === 'Owner'">

5
src/Squidex/app/shell/pages/internal/apps-menu.component.html

@ -36,7 +36,10 @@
</div>
<div class="modal-body">
<sqx-app-form (created)="modalDialog.hide()" (cancelled)="modalDialog.hide()"></sqx-app-form>
<sqx-app-form
(created)="modalDialog.hide()"
(cancelled)="modalDialog.hide()">
</sqx-app-form>
</div>
</div>
</div>

6
src/Squidex/app/theme/_mixins.scss

@ -95,6 +95,12 @@
flex-grow: $int;
}
@mixin flex-shrink($int: 0) {
-webkit-flex-shrink: $int;
-moz-flex-shrink: $int;
flex-shrink: $int;
}
@mixin inner-border($color: #999) {
-webkit-box-shadow: inset 1px 1px 1px $color;
-moz-box-shadow: inset 1px 1px 1px $color;

56
src/Squidex/app/theme/_rules.scss

@ -0,0 +1,56 @@
@import '_mixins';
@import '_vars';
$trigger-content-changed-text: #3389ff;
$trigger-content-changed-icon: #297ff6;
$action-webhook-text: #4bb958;
$action-webhook-icon: #47b353;
@mixin build-element($text-color, $icon-color) {
& {
background: $text-color;
}
&:hover {
background: $icon-color;
}
.rule-element-icon {
background: $icon-color;
}
}
.rule-element {
& {
@include truncate;
@include transition(background-color .4s ease);
color: $color-dark-foreground;
cursor: pointer;
display: inline-block;
line-height: 2.8rem;
font-size: 1rem;
font-weight: normal;
padding-right: .8rem;
width: 15rem;
}
&-icon {
height: 3rem;
display: inline-block;
line-height: 3rem;
font-size: 1.2rem;
font-weight: normal;
margin: 0;
margin-right: .5rem;
padding: 0 .8rem;
}
}
.rule-element-ContentChanged {
@include build-element($trigger-content-changed-text, $trigger-content-changed-icon)
}
.rule-element-Webhook {
@include build-element($action-webhook-text, $action-webhook-icon)
}

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

@ -1008,6 +1008,22 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-trigger-ContentChanged">
</span>
<span class="mls"> icon-trigger-ContentChanged</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e946" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe946;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-control-Date">
@ -1120,6 +1136,38 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-rules">
</span>
<span class="mls"> icon-rules</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e947" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe947;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-action-Webhook">
</span>
<span class="mls"> icon-action-Webhook</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e947" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe947;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
</div>
<div class="clearfix mhl ptl">
<h1 class="mvm mtn fgc1">Grid Size: 16</h1>

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

Binary file not shown.

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

@ -77,8 +77,8 @@
<glyph unicode="&#xe943;" glyph-name="elapsed" d="M512.002 766.788v65.212h128v64c0 35.346-28.654 64-64.002 64h-191.998c-35.346 0-64-28.654-64-64v-64h128v-65.212c-214.798-16.338-384-195.802-384-414.788 0-229.75 186.25-416 416-416s416 186.25 416 416c0 218.984-169.202 398.448-384 414.788zM706.276 125.726c-60.442-60.44-140.798-93.726-226.274-93.726s-165.834 33.286-226.274 93.726c-60.44 60.44-93.726 140.8-93.726 226.274s33.286 165.834 93.726 226.274c58.040 58.038 134.448 91.018 216.114 93.548l-21.678-314.020c-1.86-26.29 12.464-37.802 31.836-37.802s33.698 11.512 31.836 37.802l-21.676 314.022c81.666-2.532 158.076-35.512 216.116-93.55 60.44-60.44 93.726-140.8 93.726-226.274s-33.286-165.834-93.726-226.274z" />
<glyph unicode="&#xe944;" glyph-name="timeout" d="M512 832c-247.424 0-448-200.576-448-448s200.576-448 448-448 448 200.576 448 448-200.576 448-448 448zM512 24c-198.824 0-360 161.178-360 360 0 198.824 161.176 360 360 360 198.822 0 360-161.176 360-360 0-198.822-161.178-360-360-360zM934.784 672.826c16.042 28.052 25.216 60.542 25.216 95.174 0 106.040-85.96 192-192 192-61.818 0-116.802-29.222-151.92-74.596 131.884-27.236 245.206-105.198 318.704-212.578v0zM407.92 885.404c-35.116 45.374-90.102 74.596-151.92 74.596-106.040 0-192-85.96-192-192 0-34.632 9.174-67.122 25.216-95.174 73.5 107.38 186.822 185.342 318.704 212.578zM512 384v256h-64v-320h256v64z" />
<glyph unicode="&#xe945;" glyph-name="api" d="M592 557.257h-156.8c-57.6 0-105.6 48-105.6 105.6v182.4c0 57.6 48 105.6 105.6 105.6h156.8c57.6 0 105.6-48 105.6-105.6v-182.4c-3.2-57.6-48-105.6-105.6-105.6zM432 886.857c-22.4 0-41.6-19.2-41.6-41.6v-182.4c0-22.4 19.2-41.6 41.6-41.6h156.8c22.4 0 41.6 19.2 41.6 41.6v182.4c0 22.4-19.2 41.6-41.6 41.6h-156.8zM195.2-73.143c-105.6 0-195.2 89.6-195.2 195.2 0 108.8 89.6 195.2 195.2 195.2s195.2-89.6 195.2-195.2c3.2-105.6-86.4-195.2-195.2-195.2zM195.2 256.457c-73.6 0-131.2-60.8-131.2-131.2 0-73.6 60.8-134.4 131.2-134.4 73.6 0 131.2 60.8 131.2 131.2 3.2 73.6-57.6 134.4-131.2 134.4zM828.8-73.143c-108.8 0-195.2 89.6-195.2 195.2 0 108.8 89.6 195.2 195.2 195.2s195.2-89.6 195.2-195.2c0-105.6-89.6-195.2-195.2-195.2zM828.8 256.457c-73.6 0-131.2-60.8-131.2-131.2 0-73.6 60.8-131.2 131.2-131.2 73.6 0 131.2 60.8 131.2 131.2s-60.8 131.2-131.2 131.2zM332.8 310.857c-6.4 0-12.8 0-16 3.2-16 9.6-19.2 28.8-9.6 44.8l83.2 137.6c9.6 16 28.8 19.2 44.8 9.6s19.2-28.8 9.6-44.8l-83.2-137.6c-6.4-6.4-16-12.8-28.8-12.8zM691.2 310.857c-9.6 0-22.4 6.4-28.8 16l-83.2 137.6c-9.6 16-3.2 35.2 9.6 44.8s35.2 3.2 44.8-9.6l83.2-137.6c9.6-16 3.2-35.2-9.6-44.8-6.4-6.4-12.8-6.4-16-6.4z" />
<glyph unicode="&#xe946;" glyph-name="contents" d="M800-73.143h-576c-124.8 0-224 99.2-224 224v576c0 124.8 99.2 224 224 224h576c124.8 0 224-99.2 224-224v-576c0-124.8-99.2-224-224-224zM224 886.857c-89.6 0-160-70.4-160-160v-576c0-89.6 70.4-160 160-160h576c89.6 0 160 70.4 160 160v576c0 89.6-70.4 160-160 160h-576zM480 502.857h-211.2c-57.6 0-105.6 48-105.6 105.6v73.6c0 57.6 48 105.6 105.6 105.6h211.2c57.6 0 105.6-48 105.6-105.6v-73.6c0-57.6-48-105.6-105.6-105.6zM268.8 723.657c-22.4 0-41.6-19.2-41.6-41.6v-73.6c0-22.4 19.2-41.6 41.6-41.6h211.2c22.4 0 41.6 19.2 41.6 41.6v73.6c0 22.4-19.2 41.6-41.6 41.6h-211.2zM828.8 339.657h-633.6c-19.2 0-32 12.8-32 32s12.8 32 32 32h630.4c19.2 0 32-12.8 32-32s-12.8-32-28.8-32zM553.6 173.257h-358.4c-19.2 0-32 12.8-32 32s12.8 32 32 32h355.2c19.2 0 32-12.8 32-32s-12.8-32-28.8-32z" />
<glyph unicode="&#xe947;" glyph-name="webhooks" d="M217.6-41.143c-3.2 0-3.2 0-6.4 0h-3.2c-144 25.6-208 144-208 249.6 0 99.2 57.6 208 185.6 240v147.2c0 19.2 12.8 32 32 32s32-12.8 32-32v-172.8c0-16-12.8-28.8-25.6-32-108.8-16-160-102.4-160-182.4s48-166.4 153.6-185.6h6.4c16-3.2 28.8-19.2 25.6-38.4-3.2-16-16-25.6-32-25.6zM774.4-50.743c0 0 0 0 0 0-102.4 0-211.2 60.8-243.2 185.6h-176c-19.2 0-32 12.8-32 32s12.8 32 32 32h201.6c16 0 28.8-12.8 32-25.6 16-108.8 102.4-156.8 182.4-160 80 0 166.4 48 185.6 153.6v3.2c3.2 16 19.2 28.8 38.4 25.6 16-3.2 28.8-19.2 25.6-38.4v-3.2c-22.4-140.8-140.8-204.8-246.4-204.8zM787.2 272.457c-19.2 0-32 12.8-32 32v176c0 16 12.8 28.8 25.6 32 108.8 16 156.8 102.4 160 182.4 0 80-48 166.4-153.6 185.6h-3.2c-19.2 6.4-32 22.4-28.8 38.4s19.2 28.8 38.4 25.6h3.2c144-25.6 208-144 208-249.6 0-99.2-60.8-208-185.6-240v-150.4c0-16-16-32-32-32zM41.6 704.457c-3.2 0-3.2 0-6.4 0-16 3.2-28.8 19.2-25.6 35.2v3.2c25.6 144 140.8 208 246.4 208 0 0 3.2 0 3.2 0 99.2 0 208-60.8 240-185.6h147.2c19.2 0 32-12.8 32-32s-12.8-32-32-32h-172.8c-16 0-28.8 12.8-32 25.6-16 108.8-102.4 156.8-182.4 160-80 0-166.4-48-185.6-153.6v-3.2c-3.2-16-16-25.6-32-25.6zM256 563.657c-32 0-67.2 12.8-92.8 38.4-51.2 51.2-51.2 134.4 0 185.6 25.6 22.4 57.6 35.2 92.8 35.2s67.2-12.8 92.8-38.4c25.6-25.6 38.4-57.6 38.4-92.8s-12.8-67.2-38.4-92.8c-25.6-22.4-57.6-35.2-92.8-35.2zM256 758.857c-16 0-32-6.4-44.8-19.2-25.6-25.6-25.6-67.2 0-92.8s67.2-25.6 92.8 0c12.8 12.8 19.2 28.8 19.2 48s-6.4 32-19.2 44.8-28.8 19.2-48 19.2zM771.2 77.257c-32 0-67.2 12.8-92.8 38.4-51.2 51.2-51.2 134.4 0 185.6 25.6 25.6 57.6 38.4 92.8 38.4s67.2-12.8 92.8-38.4c25.6-25.6 38.4-57.6 38.4-92.8s-12.8-67.2-38.4-92.8c-28.8-25.6-60.8-38.4-92.8-38.4zM771.2 272.457c-19.2 0-35.2-6.4-48-19.2-25.6-25.6-25.6-67.2 0-92.8s67.2-25.6 92.8 0c12.8 12.8 19.2 28.8 19.2 48s-6.4 35.2-19.2 48-28.8 16-44.8 16zM745.6 563.657c-32 0-67.2 12.8-92.8 38.4s-38.4 57.6-38.4 92.8 12.8 67.2 38.4 92.8c25.6 22.4 60.8 35.2 92.8 35.2s67.2-12.8 92.8-38.4c51.2-51.2 51.2-134.4 0-185.6v0c-25.6-22.4-57.6-35.2-92.8-35.2zM745.6 758.857c-19.2 0-35.2-6.4-48-19.2s-19.2-28.8-19.2-48 6.4-35.2 19.2-48c25.6-25.6 67.2-25.6 92.8 0s25.6 67.2 0 92.8c-9.6 16-25.6 22.4-44.8 22.4zM259.2 77.257c-32 0-67.2 12.8-92.8 38.4s-38.4 57.6-38.4 92.8 12.8 67.2 38.4 92.8c25.6 22.4 57.6 35.2 92.8 35.2s67.2-12.8 92.8-38.4c51.2-51.2 51.2-134.4 0-185.6v0c-25.6-22.4-57.6-35.2-92.8-35.2zM259.2 272.457c-19.2 0-35.2-6.4-48-19.2s-19.2-28.8-19.2-48 6.4-35.2 19.2-48c25.6-25.6 67.2-25.6 92.8 0s25.6 67.2 0 92.8c-9.6 16-25.6 22.4-44.8 22.4z" />
<glyph unicode="&#xe946;" glyph-name="contents, trigger-ContentChanged" d="M800-73.143h-576c-124.8 0-224 99.2-224 224v576c0 124.8 99.2 224 224 224h576c124.8 0 224-99.2 224-224v-576c0-124.8-99.2-224-224-224zM224 886.857c-89.6 0-160-70.4-160-160v-576c0-89.6 70.4-160 160-160h576c89.6 0 160 70.4 160 160v576c0 89.6-70.4 160-160 160h-576zM480 502.857h-211.2c-57.6 0-105.6 48-105.6 105.6v73.6c0 57.6 48 105.6 105.6 105.6h211.2c57.6 0 105.6-48 105.6-105.6v-73.6c0-57.6-48-105.6-105.6-105.6zM268.8 723.657c-22.4 0-41.6-19.2-41.6-41.6v-73.6c0-22.4 19.2-41.6 41.6-41.6h211.2c22.4 0 41.6 19.2 41.6 41.6v73.6c0 22.4-19.2 41.6-41.6 41.6h-211.2zM828.8 339.657h-633.6c-19.2 0-32 12.8-32 32s12.8 32 32 32h630.4c19.2 0 32-12.8 32-32s-12.8-32-28.8-32zM553.6 173.257h-358.4c-19.2 0-32 12.8-32 32s12.8 32 32 32h355.2c19.2 0 32-12.8 32-32s-12.8-32-28.8-32z" />
<glyph unicode="&#xe947;" glyph-name="webhooks, rules, action-Webhook" d="M217.6-41.143c-3.2 0-3.2 0-6.4 0h-3.2c-144 25.6-208 144-208 249.6 0 99.2 57.6 208 185.6 240v147.2c0 19.2 12.8 32 32 32s32-12.8 32-32v-172.8c0-16-12.8-28.8-25.6-32-108.8-16-160-102.4-160-182.4s48-166.4 153.6-185.6h6.4c16-3.2 28.8-19.2 25.6-38.4-3.2-16-16-25.6-32-25.6zM774.4-50.743c0 0 0 0 0 0-102.4 0-211.2 60.8-243.2 185.6h-176c-19.2 0-32 12.8-32 32s12.8 32 32 32h201.6c16 0 28.8-12.8 32-25.6 16-108.8 102.4-156.8 182.4-160 80 0 166.4 48 185.6 153.6v3.2c3.2 16 19.2 28.8 38.4 25.6 16-3.2 28.8-19.2 25.6-38.4v-3.2c-22.4-140.8-140.8-204.8-246.4-204.8zM787.2 272.457c-19.2 0-32 12.8-32 32v176c0 16 12.8 28.8 25.6 32 108.8 16 156.8 102.4 160 182.4 0 80-48 166.4-153.6 185.6h-3.2c-19.2 6.4-32 22.4-28.8 38.4s19.2 28.8 38.4 25.6h3.2c144-25.6 208-144 208-249.6 0-99.2-60.8-208-185.6-240v-150.4c0-16-16-32-32-32zM41.6 704.457c-3.2 0-3.2 0-6.4 0-16 3.2-28.8 19.2-25.6 35.2v3.2c25.6 144 140.8 208 246.4 208 0 0 3.2 0 3.2 0 99.2 0 208-60.8 240-185.6h147.2c19.2 0 32-12.8 32-32s-12.8-32-32-32h-172.8c-16 0-28.8 12.8-32 25.6-16 108.8-102.4 156.8-182.4 160-80 0-166.4-48-185.6-153.6v-3.2c-3.2-16-16-25.6-32-25.6zM256 563.657c-32 0-67.2 12.8-92.8 38.4-51.2 51.2-51.2 134.4 0 185.6 25.6 22.4 57.6 35.2 92.8 35.2s67.2-12.8 92.8-38.4c25.6-25.6 38.4-57.6 38.4-92.8s-12.8-67.2-38.4-92.8c-25.6-22.4-57.6-35.2-92.8-35.2zM256 758.857c-16 0-32-6.4-44.8-19.2-25.6-25.6-25.6-67.2 0-92.8s67.2-25.6 92.8 0c12.8 12.8 19.2 28.8 19.2 48s-6.4 32-19.2 44.8-28.8 19.2-48 19.2zM771.2 77.257c-32 0-67.2 12.8-92.8 38.4-51.2 51.2-51.2 134.4 0 185.6 25.6 25.6 57.6 38.4 92.8 38.4s67.2-12.8 92.8-38.4c25.6-25.6 38.4-57.6 38.4-92.8s-12.8-67.2-38.4-92.8c-28.8-25.6-60.8-38.4-92.8-38.4zM771.2 272.457c-19.2 0-35.2-6.4-48-19.2-25.6-25.6-25.6-67.2 0-92.8s67.2-25.6 92.8 0c12.8 12.8 19.2 28.8 19.2 48s-6.4 35.2-19.2 48-28.8 16-44.8 16zM745.6 563.657c-32 0-67.2 12.8-92.8 38.4s-38.4 57.6-38.4 92.8 12.8 67.2 38.4 92.8c25.6 22.4 60.8 35.2 92.8 35.2s67.2-12.8 92.8-38.4c51.2-51.2 51.2-134.4 0-185.6v0c-25.6-22.4-57.6-35.2-92.8-35.2zM745.6 758.857c-19.2 0-35.2-6.4-48-19.2s-19.2-28.8-19.2-48 6.4-35.2 19.2-48c25.6-25.6 67.2-25.6 92.8 0s25.6 67.2 0 92.8c-9.6 16-25.6 22.4-44.8 22.4zM259.2 77.257c-32 0-67.2 12.8-92.8 38.4s-38.4 57.6-38.4 92.8 12.8 67.2 38.4 92.8c25.6 22.4 57.6 35.2 92.8 35.2s67.2-12.8 92.8-38.4c51.2-51.2 51.2-134.4 0-185.6v0c-25.6-22.4-57.6-35.2-92.8-35.2zM259.2 272.457c-19.2 0-35.2-6.4-48-19.2s-19.2-28.8-19.2-48 6.4-35.2 19.2-48c25.6-25.6 67.2-25.6 92.8 0s25.6 67.2 0 92.8c-9.6 16-25.6 22.4-44.8 22.4z" />
<glyph unicode="&#xe948;" glyph-name="assets" d="M800-73.143h-576c-124.8 0-224 99.2-224 224v576c0 124.8 99.2 224 224 224h576c124.8 0 224-99.2 224-224v-576c0-124.8-99.2-224-224-224zM224 886.857c-89.6 0-160-70.4-160-160v-576c0-89.6 70.4-160 160-160h576c89.6 0 160 70.4 160 160v576c0 89.6-70.4 160-160 160h-576zM771.2 90.057h-438.4c-12.8 0-22.4 6.4-28.8 19.2s-3.2 25.6 3.2 35.2l300.8 355.2c6.4 6.4 16 12.8 25.6 12.8s19.2-6.4 25.6-12.8l192-275.2c3.2-3.2 3.2-6.4 3.2-9.6 16-44.8 3.2-73.6-6.4-89.6-22.4-32-70.4-35.2-76.8-35.2zM403.2 154.057h371.2c6.4 0 22.4 3.2 25.6 9.6 3.2 3.2 3.2 12.8 0 25.6l-166.4 236.8-230.4-272zM332.8 448.457c-76.8 0-140.8 64-140.8 140.8s64 140.8 140.8 140.8 140.8-64 140.8-140.8-60.8-140.8-140.8-140.8zM332.8 666.057c-41.6 0-76.8-32-76.8-76.8s35.2-76.8 76.8-76.8 76.8 35.2 76.8 76.8-32 76.8-76.8 76.8z" />
<glyph unicode="&#xe949;" glyph-name="document-lock" d="M358.4 848.457c-28.314 0-51.2-22.886-51.2-51.2v-204.8h51.2v204.8h307.2v-153.6c0-28.314 22.886-51.2 51.2-51.2h153.6v-512h-307.2v-51.2h307.2c28.314 0 51.2 22.886 51.2 51.2v548.2l-219.8 219.8h-292.2zM716.8 761.057l117.4-117.4h-117.4zM153.6 310.857v-281.6h358.4v281.6zM179.2 310.857v76.8c0 84.48 69.12 153.6 153.6 153.6s153.6-69.12 153.6-153.6v-76.8h-51.2v76.8c0 56.32-46.080 102.4-102.4 102.4s-102.4-46.080-102.4-102.4v-76.8z" />
<glyph unicode="&#xe94a;" glyph-name="type-Tags" d="M295.954 137.249h-94.705c-47.353 0-88.786 41.434-88.786 88.786v491.283c0 47.353 41.434 88.786 88.786 88.786h94.705v59.191h-94.705c-82.867 0-147.977-65.11-147.977-147.977v-491.283c0-82.867 65.11-147.977 147.977-147.977h94.705v59.191zM970.728 486.474c-82.867 171.653-201.249 378.821-272.277 378.821h-112.462v-59.191h112.462c35.514-11.838 136.139-177.572 213.087-337.387-76.948-153.896-177.572-325.549-213.087-337.387h-112.462v-59.191h112.462c71.029 0 183.491 207.168 272.277 384.74l5.919 11.838-5.919 17.757zM266.358 622.659v-260.462h59.191v260.462h-59.191zM479.422 622.659v-260.462h59.191v260.462h-59.191z" />

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 67 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.

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

@ -1613,7 +1613,7 @@
"properties": {
"order": 93,
"id": 204,
"name": "contents",
"name": "contents, trigger-ContentChanged",
"prevSize": 32,
"code": 59718
},
@ -1835,7 +1835,7 @@
"properties": {
"order": 92,
"id": 203,
"name": "webhooks",
"name": "webhooks, rules, action-Webhook",
"prevSize": 32,
"code": 59719
},

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

@ -1,10 +1,10 @@
@font-face {
font-family: 'icomoon';
src: url('fonts/icomoon.eot?2889ep');
src: url('fonts/icomoon.eot?2889ep#iefix') format('embedded-opentype'),
url('fonts/icomoon.ttf?2889ep') format('truetype'),
url('fonts/icomoon.woff?2889ep') format('woff'),
url('fonts/icomoon.svg?2889ep#icomoon') format('svg');
src: url('fonts/icomoon.eot?5mo1zc');
src: url('fonts/icomoon.eot?5mo1zc#iefix') format('embedded-opentype'),
url('fonts/icomoon.ttf?5mo1zc') format('truetype'),
url('fonts/icomoon.woff?5mo1zc') format('woff'),
url('fonts/icomoon.svg?5mo1zc#icomoon') format('svg');
font-weight: normal;
font-style: normal;
}
@ -211,6 +211,9 @@
.icon-contents:before {
content: "\e946";
}
.icon-trigger-ContentChanged:before {
content: "\e946";
}
.icon-control-Date:before {
content: "\e936";
}
@ -232,6 +235,12 @@
.icon-webhooks:before {
content: "\e947";
}
.icon-rules:before {
content: "\e947";
}
.icon-action-Webhook:before {
content: "\e947";
}
.icon-bin2:before {
content: "\e902";
}

1
src/Squidex/app/theme/theme.scss

@ -19,4 +19,5 @@
@import '_panels';
@import '_forms';
@import '_lists';
@import '_rules';
@import '_static';
Loading…
Cancel
Save