diff --git a/src/Squidex/Areas/Api/Controllers/ApiController.cs b/src/Squidex/Areas/Api/Controllers/ApiController.cs index 0108217ee..0d8cee479 100644 --- a/src/Squidex/Areas/Api/Controllers/ApiController.cs +++ b/src/Squidex/Areas/Api/Controllers/ApiController.cs @@ -16,6 +16,7 @@ using Squidex.Pipeline; namespace Squidex.Areas.Api.Controllers { [Area("Api")] + [ApiModelValidation(false)] public abstract class ApiController : Controller { protected ICommandBus CommandBus { get; } diff --git a/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs b/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs index b3d7c33a7..428edc6c2 100644 --- a/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs +++ b/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs @@ -24,7 +24,7 @@ namespace Squidex.Areas.Api.Controllers.Users { [ApiAuthorize] [ApiExceptionFilter] - [ApiModelValidation] + [ApiModelValidation(true)] [MustBeAdministrator] [SwaggerIgnore] public sealed class UserManagementController : ApiController diff --git a/src/Squidex/Config/Orleans/SiloWrapper.cs b/src/Squidex/Config/Orleans/SiloWrapper.cs index 3b6c92dbe..df0694ad9 100644 --- a/src/Squidex/Config/Orleans/SiloWrapper.cs +++ b/src/Squidex/Config/Orleans/SiloWrapper.cs @@ -75,7 +75,7 @@ namespace Squidex.Config.Orleans { builder.AddConfiguration(hostingContext.Configuration.GetSection("logging")); builder.AddSemanticLog(); - builder.AddFilter("Orleans.Runtime.SiloControl", LogLevel.Warning); + builder.AddFilter((category, level) => !category.StartsWith("Orleans.", StringComparison.CurrentCultureIgnoreCase) || level >= LogLevel.Warning); }) .ConfigureApplicationParts(builder => { diff --git a/src/Squidex/Pipeline/ApiModelValidationAttribute.cs b/src/Squidex/Pipeline/ApiModelValidationAttribute.cs index 8b62de32f..65f88df61 100644 --- a/src/Squidex/Pipeline/ApiModelValidationAttribute.cs +++ b/src/Squidex/Pipeline/ApiModelValidationAttribute.cs @@ -7,12 +7,20 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Mvc.Filters; +using Newtonsoft.Json; using Squidex.Infrastructure; namespace Squidex.Pipeline { public sealed class ApiModelValidationAttribute : ActionFilterAttribute { + private readonly bool allErrors; + + public ApiModelValidationAttribute(bool allErrors) + { + this.allErrors = allErrors; + } + public override void OnActionExecuting(ActionExecutingContext context) { if (!context.ModelState.IsValid) @@ -23,7 +31,7 @@ namespace Squidex.Pipeline { foreach (var e in m.Value.Errors) { - if (!string.IsNullOrWhiteSpace(e.ErrorMessage)) + if (!string.IsNullOrWhiteSpace(e.ErrorMessage) && (allErrors || e.Exception is JsonException)) { errors.Add(new ValidationError(e.ErrorMessage, m.Key)); } diff --git a/src/Squidex/Program.cs b/src/Squidex/Program.cs index cde637931..49c6de70c 100644 --- a/src/Squidex/Program.cs +++ b/src/Squidex/Program.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.IO; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; @@ -28,6 +29,7 @@ namespace Squidex { builder.AddConfiguration(hostingContext.Configuration.GetSection("logging")); builder.AddSemanticLog(); + builder.AddFilter((category, level) => !category.StartsWith("Orleans.", StringComparison.CurrentCultureIgnoreCase) || level >= LogLevel.Warning); }) .ConfigureAppConfiguration((hostContext, builder) => { diff --git a/src/Squidex/app/features/content/pages/contents/contents-page.component.ts b/src/Squidex/app/features/content/pages/contents/contents-page.component.ts index 8d5f2c501..b285522a8 100644 --- a/src/Squidex/app/features/content/pages/contents/contents-page.component.ts +++ b/src/Squidex/app/features/content/pages/contents/contents-page.component.ts @@ -73,7 +73,7 @@ export class ContentsPageComponent implements OnDestroy, OnInit { this.schema = schema!; - this.contentsState.load().onErrorResumeNext().subscribe(); + this.contentsState.init().onErrorResumeNext().subscribe(); }); this.contentsSubscription = @@ -88,8 +88,6 @@ export class ContentsPageComponent implements OnDestroy, OnInit { this.languages = languages.map(x => x.language); this.language = this.languages.at(0); }); - - this.contentsState.load().onErrorResumeNext().subscribe(); } public reload() { diff --git a/src/Squidex/app/features/content/shared/references-editor.component.scss b/src/Squidex/app/features/content/shared/references-editor.component.scss index b5ec83b77..1ff195b24 100644 --- a/src/Squidex/app/features/content/shared/references-editor.component.scss +++ b/src/Squidex/app/features/content/shared/references-editor.component.scss @@ -18,7 +18,7 @@ overflow-x: hidden; overflow-y: auto; padding: 1rem; - min-height: 2rem; + min-height: 6rem; max-height: 20rem; } diff --git a/src/Squidex/app/features/rules/pages/events/rule-events-page.component.html b/src/Squidex/app/features/rules/pages/events/rule-events-page.component.html index cb5a586d8..9c6b6288f 100644 --- a/src/Squidex/app/features/rules/pages/events/rule-events-page.component.html +++ b/src/Squidex/app/features/rules/pages/events/rule-events-page.component.html @@ -6,11 +6,11 @@ - - + @@ -34,7 +34,7 @@ - + {{event.jobResult}} @@ -71,7 +71,7 @@ Next: {{event.nextAttempt | sqxFromNow}}
-
@@ -88,7 +88,7 @@ - +
diff --git a/src/Squidex/app/features/rules/pages/events/rule-events-page.component.ts b/src/Squidex/app/features/rules/pages/events/rule-events-page.component.ts index fb65ac2d5..4576ec94f 100644 --- a/src/Squidex/app/features/rules/pages/events/rule-events-page.component.ts +++ b/src/Squidex/app/features/rules/pages/events/rule-events-page.component.ts @@ -9,11 +9,8 @@ import { Component, OnInit } from '@angular/core'; import { AppsState, - DialogService, - ImmutableArray, - Pager, RuleEventDto, - RulesService + RuleEventsState } from '@app/shared'; @Component({ @@ -22,59 +19,40 @@ import { templateUrl: './rule-events-page.component.html' }) export class RuleEventsPageComponent implements OnInit { - public eventsItems = ImmutableArray.empty(); - public eventsPager = new Pager(0); - public selectedEventId: string | null = null; constructor( public readonly appsState: AppsState, - private readonly dialogs: DialogService, - private readonly rulesService: RulesService + public readonly ruleEventsState: RuleEventsState ) { } public ngOnInit() { - this.load(); + this.ruleEventsState.load().onErrorResumeNext().subscribe(); } - public load(notifyLoad = false) { - this.rulesService.getEvents(this.appsState.appName, this.eventsPager.pageSize, this.eventsPager.skip) - .subscribe(dtos => { - this.eventsItems = ImmutableArray.of(dtos.items); - this.eventsPager = this.eventsPager.setCount(dtos.total); - - if (notifyLoad) { - this.dialogs.notifyInfo('Events reloaded.'); - } - }, error => { - this.dialogs.notifyError(error); - }); + public reload() { + this.ruleEventsState.load(true).onErrorResumeNext().subscribe(); } - public enqueueEvent(event: RuleEventDto) { - this.rulesService.enqueueEvent(this.appsState.appName, event.id) - .subscribe(() => { - this.dialogs.notifyInfo('Events enqueued. Will be resend in a few seconds.'); - }, error => { - this.dialogs.notifyError(error); - }); + public goNext() { + this.ruleEventsState.goNext().onErrorResumeNext().subscribe(); } - public selectEvent(id: string) { - this.selectedEventId = this.selectedEventId !== id ? id : null; + public goPrev() { + this.ruleEventsState.goPrev().onErrorResumeNext().subscribe(); } - public goNext() { - this.eventsPager = this.eventsPager.goNext(); - - this.load(); + public enqueue(event: RuleEventDto) { + this.ruleEventsState.enqueue(event).onErrorResumeNext().subscribe(); } - public goPrev() { - this.eventsPager = this.eventsPager.goPrev(); + public selectEvent(id: string) { + this.selectedEventId = this.selectedEventId !== id ? id : null; + } - this.load(); + public trackByRuleEvent(index: number, ruleEvent: RuleEventDto) { + return ruleEvent.id; } } diff --git a/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.ts b/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.ts index ff7ae5127..891232a55 100644 --- a/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.ts +++ b/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.ts @@ -121,6 +121,12 @@ export class RuleWizardComponent implements OnInit { this.rulesState.create(requestDto) .subscribe(dto => { this.complete(); + + this.actionForm.submitCompleted(); + this.triggerForm.submitCompleted(); + }, error => { + this.actionForm.submitFailed(error); + this.triggerForm.submitFailed(error); }); } @@ -128,6 +134,10 @@ export class RuleWizardComponent implements OnInit { this.rulesState.updateTrigger(this.rule, this.trigger) .subscribe(dto => { this.complete(); + + this.triggerForm.submitCompleted(); + }, error => { + this.triggerForm.submitFailed(error); }); } @@ -135,6 +145,10 @@ export class RuleWizardComponent implements OnInit { this.rulesState.updateAction(this.rule, this.action) .subscribe(dto => { this.complete(); + + this.actionForm.submitCompleted(); + }, error => { + this.actionForm.submitFailed(error); }); } } \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html b/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html index 657279aa3..495888ae6 100644 --- a/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html +++ b/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html @@ -1,6 +1,6 @@

Trigger rule when an events for a schemas happens

- + diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts b/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts index 10ee7a03e..52fa46cee 100644 --- a/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts +++ b/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts @@ -52,7 +52,7 @@ export class ContentChangedTriggerComponent implements OnInit { public ngOnInit() { this.triggerForm.setControl('schemas', - new FormControl(this.trigger.schemas || {})); + new FormControl(this.trigger.schemas || [])); this.triggerForm.setControl('handleAll', new FormControl(Types.isBoolean(this.trigger.handleAll) ? this.trigger.handleAll : false)); diff --git a/src/Squidex/app/features/settings/pages/languages/language.component.html b/src/Squidex/app/features/settings/pages/languages/language.component.html index bcbbf3076..6a1c88efb 100644 --- a/src/Squidex/app/features/settings/pages/languages/language.component.html +++ b/src/Squidex/app/features/settings/pages/languages/language.component.html @@ -2,7 +2,7 @@
- + {{language.iso2Code}}
{{language.englishName}} diff --git a/src/Squidex/app/features/settings/pages/plans/plans-page.component.html b/src/Squidex/app/features/settings/pages/plans/plans-page.component.html index 59c015f4b..1bea97060 100644 --- a/src/Squidex/app/features/settings/pages/plans/plans-page.component.html +++ b/src/Squidex/app/features/settings/pages/plans/plans-page.component.html @@ -14,65 +14,67 @@ - -
- You have not created the subscription. Therefore you cannot change the planInfo.plan. -
+ + +
+ You have not created the subscription. Therefore you cannot change the planInfo.plan. +
-
- No plan configured, this app has unlimited usage. -
+
+ No plan configured, this app has unlimited usage. +
-
-
-
-

{{planInfo.plan.name}}

-
{{planInfo.plan.costs}}
+
+
+
+

{{planInfo.plan.name}}

+
{{planInfo.plan.costs}}
- Per Month -
-
-
-
- {{planInfo.plan.maxApiCalls | sqxKNumber}} API Calls -
-
- {{planInfo.plan.maxAssetSize | sqxFileSize}} Storage -
-
- {{planInfo.plan.maxContributors}} Contributors -
+ Per Month
- - +
+
+
+ {{planInfo.plan.maxApiCalls | sqxKNumber}} API Calls +
+
+ {{planInfo.plan.maxAssetSize | sqxFileSize}} Storage +
+
+ {{planInfo.plan.maxContributors}} Contributors +
+
- -
-
-
- + + \ No newline at end of file diff --git a/src/Squidex/app/shared/internal.ts b/src/Squidex/app/shared/internal.ts index 6226b1dfc..439b10224 100644 --- a/src/Squidex/app/shared/internal.ts +++ b/src/Squidex/app/shared/internal.ts @@ -48,6 +48,7 @@ export * from './state/contributors.state'; export * from './state/languages.state'; export * from './state/patterns.state'; export * from './state/plans.state'; +export * from './state/rule-events.state'; export * from './state/rules.state'; export * from './state/schemas.state'; diff --git a/src/Squidex/app/shared/module.ts b/src/Squidex/app/shared/module.ts index 300ada909..3997d0e01 100644 --- a/src/Squidex/app/shared/module.ts +++ b/src/Squidex/app/shared/module.ts @@ -57,6 +57,7 @@ import { PlansService, PlansState, RichEditorComponent, + RuleEventsState, RulesService, RulesState, SchemaMustExistGuard, @@ -164,6 +165,7 @@ export class SqxSharedModule { PatternsState, PlansService, PlansState, + RuleEventsState, RulesService, RulesState, SchemaMustExistGuard, diff --git a/src/Squidex/app/shared/state/contents.state.ts b/src/Squidex/app/shared/state/contents.state.ts index 56d2fcce8..9304f132a 100644 --- a/src/Squidex/app/shared/state/contents.state.ts +++ b/src/Squidex/app/shared/state/contents.state.ts @@ -29,10 +29,7 @@ import { fieldInvariant, SchemaDetailsDto, SchemaDto } from './../services/schem import { AppsState } from './apps.state'; import { SchemasState } from './schemas.state'; -import { - ContentDto, - ContentsService -} from './../services/contents.service'; +import { ContentDto, ContentsService } from './../services/contents.service'; export class EditContentForm extends Form { constructor( @@ -317,6 +314,12 @@ export abstract class ContentsStateBase extends State { return this.load(); } + public init(): Observable { + this.next(s => ({ ...s, contentsPager: new Pager(0), contentsQuery: '', isArchive: false, isLoaded: false })); + + return this.load(); + } + public search(query: string): Observable { this.next(s => ({ ...s, contentsPager: new Pager(0), contentsQuery: query })); diff --git a/src/Squidex/app/shared/state/rule-events.state.spec.ts b/src/Squidex/app/shared/state/rule-events.state.spec.ts new file mode 100644 index 000000000..e0cbbfc60 --- /dev/null +++ b/src/Squidex/app/shared/state/rule-events.state.spec.ts @@ -0,0 +1,88 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Observable } from 'rxjs'; +import { IMock, It, Mock, Times } from 'typemoq'; + +import { + AppsState, + DateTime, + DialogService +} from '@app/shared'; + +import { RuleEventsState } from './rule-events.state'; + +import { + RuleEventDto, + RuleEventsDto, + RulesService +} from './../services/rules.service'; + +describe('RuleEventsState', () => { + const app = 'my-app'; + + const oldRuleEvents = [ + new RuleEventDto('id1', DateTime.now(), null, 'event1', 'description', 'dump1', 'result1', 'result1', 1), + new RuleEventDto('id2', DateTime.now(), null, 'event2', 'description', 'dump2', 'result2', 'result2', 2) + ]; + + let appsState: IMock; + let dialogs: IMock; + let rulesService: IMock; + let ruleEventsState: RuleEventsState; + + beforeEach(() => { + dialogs = Mock.ofType(); + + appsState = Mock.ofType(); + + appsState.setup(x => x.appName) + .returns(() => app); + + rulesService = Mock.ofType(); + + rulesService.setup(x => x.getEvents(app, 10, 0)) + .returns(() => Observable.of(new RuleEventsDto(200, oldRuleEvents))); + + ruleEventsState = new RuleEventsState(appsState.object, dialogs.object, rulesService.object); + ruleEventsState.load().subscribe(); + }); + + it('should load ruleEvents', () => { + expect(ruleEventsState.snapshot.ruleEvents.values).toEqual(oldRuleEvents); + expect(ruleEventsState.snapshot.ruleEventsPager.numberOfItems).toEqual(200); + expect(ruleEventsState.snapshot.isLoaded).toBeTruthy(); + + dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never()); + }); + + it('should show notification on load when flag is true', () => { + ruleEventsState.load(true).subscribe(); + + dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once()); + }); + + it('should load next page and prev page when paging', () => { + rulesService.setup(x => x.getEvents(app, 10, 10)) + .returns(() => Observable.of(new RuleEventsDto(200, []))); + + ruleEventsState.goNext().subscribe(); + ruleEventsState.goPrev().subscribe(); + + rulesService.verify(x => x.getEvents(app, 10, 10), Times.once()); + rulesService.verify(x => x.getEvents(app, 10, 0), Times.exactly(2)); + }); + + it('should call service when enqueuing event', () => { + rulesService.setup(x => x.enqueueEvent(app, oldRuleEvents[0].id)) + .returns(() => Observable.of({})); + + ruleEventsState.enqueue(oldRuleEvents[0]).subscribe(); + + rulesService.verify(x => x.enqueueEvent(app, oldRuleEvents[0].id), Times.once()); + }); +}); \ No newline at end of file diff --git a/src/Squidex/app/shared/state/rule-events.state.ts b/src/Squidex/app/shared/state/rule-events.state.ts new file mode 100644 index 000000000..408eb9975 --- /dev/null +++ b/src/Squidex/app/shared/state/rule-events.state.ts @@ -0,0 +1,95 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; + +import '@app/framework/utils/rxjs-extensions'; + +import { + DialogService, + ImmutableArray, + Pager, + State +} from '@app/framework'; + +import { AppsState } from './apps.state'; + +import { RuleEventDto, RulesService } from './../services/rules.service'; + +interface Snapshot { + ruleEvents: ImmutableArray; + ruleEventsPager: Pager; + + isLoaded?: boolean; +} + +@Injectable() +export class RuleEventsState extends State { + public ruleEvents = + this.changes.map(x => x.ruleEvents) + .distinctUntilChanged(); + + public ruleEventsPager = + this.changes.map(x => x.ruleEventsPager) + .distinctUntilChanged(); + + public isLoaded = + this.changes.map(x => !!x.isLoaded) + .distinctUntilChanged(); + + constructor( + private readonly appsState: AppsState, + private readonly dialogs: DialogService, + private readonly rulesService: RulesService + ) { + super({ ruleEvents: ImmutableArray.of(), ruleEventsPager: new Pager(0) }); + } + + public load(notifyLoad = false): Observable { + return this.rulesService.getEvents(this.appName, + this.snapshot.ruleEventsPager.pageSize, + this.snapshot.ruleEventsPager.skip) + .do(dtos => { + if (notifyLoad) { + this.dialogs.notifyInfo('RuleEvents reloaded.'); + } + + return this.next(s => { + const ruleEvents = ImmutableArray.of(dtos.items); + const ruleEventsPager = s.ruleEventsPager.setCount(dtos.total); + + return { ...s, ruleEvents, ruleEventsPager, isLoaded: true }; + }); + }) + .notify(this.dialogs); + } + + public enqueue(event: RuleEventDto): Observable { + return this.rulesService.enqueueEvent(this.appsState.appName, event.id) + .do(() => { + this.dialogs.notifyInfo('Events enqueued. Will be resend in a few seconds.'); + }) + .notify(this.dialogs); + } + + public goNext(): Observable { + this.next(s => ({ ...s, ruleEventsPager: s.ruleEventsPager.goNext() })); + + return this.load(); + } + + public goPrev(): Observable { + this.next(s => ({ ...s, ruleEventsPager: s.ruleEventsPager.goPrev() })); + + return this.load(); + } + + private get appName() { + return this.appsState.appName; + } +} \ No newline at end of file