diff --git a/src/Squidex.Infrastructure.MongoDb/EventStore/MongoEventStore.cs b/src/Squidex.Infrastructure.MongoDb/EventStore/MongoEventStore.cs index 7e6dac229..f545e6c21 100644 --- a/src/Squidex.Infrastructure.MongoDb/EventStore/MongoEventStore.cs +++ b/src/Squidex.Infrastructure.MongoDb/EventStore/MongoEventStore.cs @@ -93,7 +93,7 @@ namespace Squidex.Infrastructure.MongoDb.EventStore if (!string.IsNullOrWhiteSpace(streamFilter) && !string.Equals(streamFilter, "*", StringComparison.OrdinalIgnoreCase)) { - if (streamFilter.StartsWith("^")) + if (streamFilter.Contains("^")) { filters.Add(Filter.Regex(x => x.EventStream, streamFilter)); } diff --git a/src/Squidex.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs b/src/Squidex.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs index 6136473bc..97ad61133 100644 --- a/src/Squidex.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs +++ b/src/Squidex.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs @@ -98,6 +98,16 @@ namespace Squidex.Read.MongoDb.Schemas return UpdateSchema(@event, headers, s => SchemaEventDispatcher.Dispatch(@event, s, registry)); } + protected Task On(WebhookAdded @event, EnvelopeHeaders headers) + { + return UpdateSchema(@event, headers, s => s); + } + + protected Task On(WebhookDeleted @event, EnvelopeHeaders headers) + { + return UpdateSchema(@event, headers, s => s); + } + protected Task On(SchemaDeleted @event, EnvelopeHeaders headers) { return Collection.UpdateAsync(@event, headers, e => e.IsDeleted = true); diff --git a/src/Squidex.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs b/src/Squidex.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs index 37f99a672..b164036d5 100644 --- a/src/Squidex.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs +++ b/src/Squidex.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs @@ -125,6 +125,14 @@ namespace Squidex.Read.Schemas.Services.Implementations { Remove(schemaUpdatedEvent.AppId, schemaUpdatedEvent.SchemaId); } + else if (@event.Payload is WebhookAdded webhookAddedEvent) + { + Remove(webhookAddedEvent.AppId, webhookAddedEvent.SchemaId); + } + else if (@event.Payload is WebhookDeleted webhookDeletedEvent) + { + Remove(webhookDeletedEvent.AppId, webhookDeletedEvent.SchemaId); + } return TaskHelper.Done; } diff --git a/src/Squidex.Read/Schemas/WebhookInvoker.cs b/src/Squidex.Read/Schemas/WebhookInvoker.cs index c56ea4743..53295972b 100644 --- a/src/Squidex.Read/Schemas/WebhookInvoker.cs +++ b/src/Squidex.Read/Schemas/WebhookInvoker.cs @@ -82,7 +82,7 @@ namespace Squidex.Read.Schemas return new JObject( new JProperty("type", @event.Payload.GetType().Name), new JProperty("meta", JObject.FromObject(@event.Headers, webhookSerializer)), - new JProperty("data", JObject.FromObject(@event.Headers, webhookSerializer))); + new JProperty("data", JObject.FromObject(@event.Payload, webhookSerializer))); } private async Task DispatchEventAsync(JObject payload, ISchemaWebhookEntity webhook) diff --git a/src/Squidex/app/app.routes.ts b/src/Squidex/app/app.routes.ts index 72ad2d169..ebf192b20 100644 --- a/src/Squidex/app/app.routes.ts +++ b/src/Squidex/app/app.routes.ts @@ -63,6 +63,10 @@ export const routes: Routes = [ path: 'assets', loadChildren: './features/assets/module#SqxFeatureAssetsModule' }, + { + path: 'webhooks', + loadChildren: './features/webhooks/module#SqxFeatureWebhooksModule' + }, { path: 'settings', loadChildren: './features/settings/module#SqxFeatureSettingsModule' diff --git a/src/Squidex/app/features/administration/pages/users/user-page.component.ts b/src/Squidex/app/features/administration/pages/users/user-page.component.ts index 78c285cea..6b7c89a5e 100644 --- a/src/Squidex/app/features/administration/pages/users/user-page.component.ts +++ b/src/Squidex/app/features/administration/pages/users/user-page.component.ts @@ -65,7 +65,7 @@ export class UserPageComponent extends ComponentBase implements OnInit { const enable = (message?: string) => { this.userForm.enable(); - this.userForm.controls['password'].reset(); + this.userForm.get('password').reset(); this.userFormSubmitted = false; this.userFormError = message; }; diff --git a/src/Squidex/app/features/assets/pages/assets-page.component.ts b/src/Squidex/app/features/assets/pages/assets-page.component.ts index 5950183c4..ea2922b5e 100644 --- a/src/Squidex/app/features/assets/pages/assets-page.component.ts +++ b/src/Squidex/app/features/assets/pages/assets-page.component.ts @@ -17,7 +17,6 @@ import { AssetDto, AssetsService, AssetUpdated, - fadeAnimation, ImmutableArray, MessageBus, NotificationService, @@ -27,10 +26,7 @@ import { @Component({ selector: 'sqx-assets-page', styleUrls: ['./assets-page.component.scss'], - templateUrl: './assets-page.component.html', - animations: [ - fadeAnimation - ] + templateUrl: './assets-page.component.html' }) export class AssetsPageComponent extends AppComponentBase implements OnDestroy, OnInit { private assetUpdatedSubscription: Subscription; diff --git a/src/Squidex/app/features/content/pages/content/content-page.component.ts b/src/Squidex/app/features/content/pages/content/content-page.component.ts index 015ae12dc..c2fc15508 100644 --- a/src/Squidex/app/features/content/pages/content/content-page.component.ts +++ b/src/Squidex/app/features/content/pages/content/content-page.component.ts @@ -179,7 +179,7 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone this.contentForm.markAsPristine(); for (const field of this.schema.fields.filter(f => !f.isDisabled)) { - const fieldForm = this.contentForm.controls[field.name]; + const fieldForm = this.contentForm.get(field.name); fieldForm.enable(); } @@ -187,7 +187,7 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone private disable() { for (const field of this.schema.fields.filter(f => !f.isDisabled)) { - const fieldForm = this.contentForm.controls[field.name]; + const fieldForm = this.contentForm.get(field.name); fieldForm.disable(); } @@ -252,14 +252,14 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone for (const field of this.schema.fields) { const fieldValue = content.data[field.name] || {}; - const fieldForm = this.contentForm.controls[field.name]; + const fieldForm = this.contentForm.get(field.name); if (field.partitioning === 'language') { for (let language of this.languages) { - fieldForm.controls[language.iso2Code].setValue(fieldValue[language.iso2Code]); + fieldForm.get(language.iso2Code).setValue(fieldValue[language.iso2Code]); } } else { - fieldForm.controls['iv'].setValue(fieldValue['iv']); + fieldForm.get('iv').setValue(fieldValue['iv']); } } } diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts index 8553f86ff..8cfe1306a 100644 --- a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts @@ -82,7 +82,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit { }); public get hasName() { - return this.addFieldForm.controls['name'].value && this.addFieldForm.controls['name'].value.length > 0; + return this.addFieldForm.get('name').value && this.addFieldForm.get('name').value.length > 0; } constructor(apps: AppsStoreService, notifications: NotificationService, diff --git a/src/Squidex/app/features/settings/pages/clients/client.component.html b/src/Squidex/app/features/settings/pages/clients/client.component.html index 75ba7d220..4758055dd 100644 --- a/src/Squidex/app/features/settings/pages/clients/client.component.html +++ b/src/Squidex/app/features/settings/pages/clients/client.component.html @@ -47,7 +47,7 @@ Client Id: - + + + + + +

Webhooks

+ + + + + + + +
+
+
+ No Webhook created yet. +
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + +
+

+ Schema: {{webhook.schema.name}} +

+
+ +
Url: + + + +
Token: + + + +
+
+
+ + +
+
+
+ \ No newline at end of file diff --git a/src/Squidex/app/features/webhooks/pages/webhooks-page.component.scss b/src/Squidex/app/features/webhooks/pages/webhooks-page.component.scss new file mode 100644 index 000000000..7799839dd --- /dev/null +++ b/src/Squidex/app/features/webhooks/pages/webhooks-page.component.scss @@ -0,0 +1,6 @@ +@import '_vars'; +@import '_mixins'; + +.schemas-control { + width: 10rem; +} \ 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 new file mode 100644 index 000000000..7b7abb944 --- /dev/null +++ b/src/Squidex/app/features/webhooks/pages/webhooks-page.component.ts @@ -0,0 +1,126 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; + +import { + AppComponentBase, + AppsStoreService, + CreateWebhookDto, + ImmutableArray, + NotificationService, + SchemaDto, + SchemasService, + Version, + WebhookDto, + WebhooksService +} from 'shared'; + +interface WebhookWithSchema { webhook: WebhookDto; schema: SchemaDto; }; + +@Component({ + selector: 'sqx-webhooks-page', + styleUrls: ['./webhooks-page.component.scss'], + templateUrl: './webhooks-page.component.html' +}) +export class WebhooksPageComponent extends AppComponentBase implements OnInit { + private readonly version = new Version(); + + public webhooks: ImmutableArray; + + public schemas: SchemaDto[]; + + public addWebhookFormSubmitted = false; + public addWebhookForm: FormGroup = + this.formBuilder.group({ + schemaId: ['', + [ + Validators.required + ]], + url: ['', + [ + Validators.required + ]] + }); + + public get hasUrl() { + return this.addWebhookForm.get('url').value && this.addWebhookForm.get('url').value.length > 0; + } + + constructor(apps: AppsStoreService, notifications: NotificationService, + private readonly schemasService: SchemasService, + private readonly webhooksService: WebhooksService, + private readonly formBuilder: FormBuilder + ) { + super(notifications, apps); + } + + public ngOnInit() { + this.load(); + } + + public load(showInfo = false) { + this.appNameOnce() + .switchMap(app => + this.schemasService.getSchemas(app) + .withLatestFrom(this.webhooksService.getWebhooks(app), + (s, w) => { return { webhooks: w, schemas: s }; })) + .subscribe(dtos => { + this.schemas = dtos.schemas; + + this.webhooks = + ImmutableArray.of( + dtos.webhooks.map(w => { + return { webhook: w, schema: dtos.schemas.find(s => s.id === w.schemaId) }; + }).filter(w => !!w.schema)); + + if (showInfo) { + this.notifyInfo('Webhooks reloaded.'); + } + }, error => { + this.notifyError(error); + }); + } + + public resetWebhookForm() { + this.addWebhookFormSubmitted = false; + this.addWebhookForm.enable(); + this.addWebhookForm.reset(); + } + + public addWebhook() { + if (this.addWebhookForm.valid) { + this.addWebhookFormSubmitted = true; + this.addWebhookForm.disable(); + + const requestDto = new CreateWebhookDto(this.addWebhookForm.get('url')!.value); + const schemaId = this.addWebhookForm.get('schemaId')!.value; + const schema = this.schemas.find(s => s.id === schemaId); + + this.appNameOnce() + .switchMap(app => this.webhooksService.postWebhook(app, schema.name, requestDto, this.version)) + .subscribe(dto => { + this.webhooks = this.webhooks.push({ schema, webhook: dto }); + this.resetWebhookForm(); + }, error => { + this.notifyError(error); + this.resetWebhookForm(); + }); + } + } + + public deleteWebhook(webhook: WebhookWithSchema) { + this.appNameOnce() + .switchMap(app => this.webhooksService.deleteWebhook(app, webhook.schema.name, webhook.webhook.id, this.version)) + .subscribe(dto => { + this.webhooks = this.webhooks.remove(webhook); + }, error => { + this.notifyError(error); + }); + } +} diff --git a/src/Squidex/app/framework/angular/control-errors.component.ts b/src/Squidex/app/framework/angular/control-errors.component.ts index 001474b55..051c6b79e 100644 --- a/src/Squidex/app/framework/angular/control-errors.component.ts +++ b/src/Squidex/app/framework/angular/control-errors.component.ts @@ -101,6 +101,6 @@ export class ControlErrorsComponent implements OnChanges { this.displayFieldName = this.for.substr(0, 1).toUpperCase() + this.for.substr(1); } - this.control = this.formGroupDirective.form.controls[this.for]; + this.control = this.formGroupDirective.form.get(this.for); } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/geolocation-editor.component.ts b/src/Squidex/app/framework/angular/geolocation-editor.component.ts index a01e39ea9..58fc983a7 100644 --- a/src/Squidex/app/framework/angular/geolocation-editor.component.ts +++ b/src/Squidex/app/framework/angular/geolocation-editor.component.ts @@ -108,8 +108,8 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi } public updateValueByInput() { - if (this.geolocationForm.controls['latitude'].value !== null && - this.geolocationForm.controls['longitude'].value !== null && + if (this.geolocationForm.get('latitude').value !== null && + this.geolocationForm.get('longitude').value !== null && this.geolocationForm.valid) { this.value = this.geolocationForm.value; } else { diff --git a/src/Squidex/app/shared/components/asset.component.ts b/src/Squidex/app/shared/components/asset.component.ts index bd32284de..663dff1e7 100644 --- a/src/Squidex/app/shared/components/asset.component.ts +++ b/src/Squidex/app/shared/components/asset.component.ts @@ -164,7 +164,7 @@ export class AssetComponent extends AppComponentBase implements OnInit { if (this.renameForm.valid) { this.renameForm.disable(); - const requestDto = new UpdateAssetDto(this.renameForm.controls['name'].value); + const requestDto = new UpdateAssetDto(this.renameForm.get('name').value); this.appNameOnce() .switchMap(app => this.assetsService.putAsset(app, this.asset.id, requestDto, this.version)) @@ -194,7 +194,7 @@ export class AssetComponent extends AppComponentBase implements OnInit { private resetRename() { this.renameForm.enable(); - this.renameForm.controls['name'].setValue(this.asset.fileName); + this.renameForm.get('name').setValue(this.asset.fileName); this.renameFormSubmitted = false; this.renameDialog.hide(); } diff --git a/src/Squidex/app/shared/declarations-base.ts b/src/Squidex/app/shared/declarations-base.ts index 9f7f83364..59e9901a4 100644 --- a/src/Squidex/app/shared/declarations-base.ts +++ b/src/Squidex/app/shared/declarations-base.ts @@ -31,6 +31,7 @@ export * from './services/schemas.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/file-helper'; export * from './utils/messages'; diff --git a/src/Squidex/app/shared/module.ts b/src/Squidex/app/shared/module.ts index 112f8d712..551b8ace5 100644 --- a/src/Squidex/app/shared/module.ts +++ b/src/Squidex/app/shared/module.ts @@ -49,7 +49,8 @@ import { UserPictureRefPipe, UserManagementService, UsersProviderService, - UsersService + UsersService, + WebhooksService } from './declarations'; @NgModule({ @@ -117,7 +118,8 @@ export class SqxSharedModule { UsagesService, UserManagementService, UsersProviderService, - UsersService + UsersService, + WebhooksService ] }; } diff --git a/src/Squidex/app/shared/services/plans.service.ts b/src/Squidex/app/shared/services/plans.service.ts index a01a49d84..eb53552c9 100644 --- a/src/Squidex/app/shared/services/plans.service.ts +++ b/src/Squidex/app/shared/services/plans.service.ts @@ -36,7 +36,6 @@ export class PlanDto { } } - export class ChangePlanDto { constructor( public readonly planId: string diff --git a/src/Squidex/app/shared/services/webhooks.service.spec.ts b/src/Squidex/app/shared/services/webhooks.service.spec.ts new file mode 100644 index 000000000..1eff69cea --- /dev/null +++ b/src/Squidex/app/shared/services/webhooks.service.spec.ts @@ -0,0 +1,108 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Response, ResponseOptions } from '@angular/http'; +import { Observable } from 'rxjs'; +import { IMock, Mock, Times } from 'typemoq'; + +import { + ApiUrlConfig, + AuthService, + CreateWebhookDto, + Version, + WebhookDto, + WebhooksService +} from './../'; + +describe('WebhooksService', () => { + let authService: IMock; + let webhooksService: WebhooksService; + let version = new Version('1'); + + beforeEach(() => { + authService = Mock.ofType(AuthService); + webhooksService = new WebhooksService(authService.object, new ApiUrlConfig('http://service/p/')); + }); + + it('should make get request to get app webhooks', () => { + authService.setup(x => x.authGet('http://service/p/api/apps/my-app/webhooks', version)) + .returns(() => Observable.of( + new Response( + new ResponseOptions({ + body: [{ + id: 'id1', + schemaId: 'schemaId1', + securityToken: 'token1', + url: 'http://squidex.io/1' + }, { + id: 'id2', + schemaId: 'schemaId2', + securityToken: 'token2', + url: 'http://squidex.io/2' + }] + }) + ) + )) + .verifiable(Times.once()); + + let webhooks: WebhookDto[] | null = null; + + webhooksService.getWebhooks('my-app', version).subscribe(result => { + webhooks = result; + }).unsubscribe(); + + expect(webhooks).toEqual([ + new WebhookDto('id1', 'schemaId1', 'token1', 'http://squidex.io/1'), + new WebhookDto('id2', 'schemaId2', 'token2', 'http://squidex.io/2') + ]); + + authService.verifyAll(); + }); + + it('should make post request to create webhook', () => { + const dto = new CreateWebhookDto('http://squidex.io/hook'); + + authService.setup(x => x.authPost('http://service/p/api/apps/my-app/schemas/my-schema/webhooks', dto, version)) + .returns(() => Observable.of( + new Response( + new ResponseOptions({ + body: { + id: 'id1', + schemaId: 'schemaId1', + securityToken: 'token1', + url: 'http://squidex.io/1' + } + }) + ) + )) + .verifiable(Times.once()); + + let webhook: WebhookDto | null = null; + + webhooksService.postWebhook('my-app', 'my-schema', dto, version).subscribe(result => { + webhook = result; + }).unsubscribe(); + + expect(webhook).toEqual(new WebhookDto('id1', 'schemaId1', 'token1', 'http://squidex.io/1')); + + authService.verifyAll(); + }); + + it('should make delete request to delete webhook', () => { + authService.setup(x => x.authDelete('http://service/p/api/apps/my-app/schemas/my-schema/webhooks/123', version)) + .returns(() => Observable.of( + new Response( + new ResponseOptions() + ) + )) + .verifiable(Times.once()); + + webhooksService.deleteWebhook('my-app', 'my-schema', '123', version); + + authService.verifyAll(); + }); +}); \ 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 new file mode 100644 index 000000000..f6fe03178 --- /dev/null +++ b/src/Squidex/app/shared/services/webhooks.service.ts @@ -0,0 +1,81 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; + +import 'framework/angular/http-extensions'; + +import { ApiUrlConfig, Version } from 'framework'; +import { AuthService } from './auth.service'; + +export class WebhookDto { + constructor( + public readonly id: string, + public readonly schemaId: string, + public readonly securityToken: string, + public readonly url: string + ) { + } +} + +export class CreateWebhookDto { + constructor( + public readonly url: string + ) { + } +} + +@Injectable() +export class WebhooksService { + constructor( + private readonly authService: AuthService, + private readonly apiUrl: ApiUrlConfig + ) { + } + + public getWebhooks(appName: string, version?: Version): Observable { + const url = this.apiUrl.buildUrl(`api/apps/${appName}/webhooks`); + + return this.authService.authGet(url, version) + .map(response => response.json()) + .map(response => { + const items: any[] = response; + + return items.map(item => { + return new WebhookDto( + item.id, + item.schemaId, + item.securityToken, + item.url); + }); + }) + .catchError('Failed to load webhooks. Please reload.'); + } + + public postWebhook(appName: string, schemaName: string, dto: CreateWebhookDto, version?: Version): Observable { + const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/webhooks`); + + return this.authService.authPost(url, dto, version) + .map(response => response.json()) + .map(response => { + return new WebhookDto( + response.id, + response.schemaId, + response.securityToken, + response.url); + }) + .catchError('Failed to create webhook. Please reload.'); + } + + public deleteWebhook(appName: string, schemaName: string, id: string, version?: Version): Observable { + const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/webhooks/${id}`); + + return this.authService.authDelete(url, version) + .catchError('Failed to delete webhook. 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 8eb971eb8..114ac9553 100644 --- a/src/Squidex/app/shell/pages/app/left-menu.component.html +++ b/src/Squidex/app/shell/pages/app/left-menu.component.html @@ -15,6 +15,11 @@ +