diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs b/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs index 4cc1b47c5..800a41687 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs @@ -59,7 +59,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.State SchemaDef = SchemaDef.AddField(field); } - SchemaFieldsTotal++; + SchemaFieldsTotal = Math.Max(SchemaFieldsTotal, @event.FieldId.Id); } protected void On(SchemaCategoryChanged @event) diff --git a/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs b/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs index 7cb74737a..e76b0d884 100644 --- a/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs +++ b/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs @@ -7,7 +7,6 @@ using System; using System.IO; -using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; @@ -141,7 +140,7 @@ namespace Squidex.Infrastructure.Assets private static string GetFileName(string id, long version, string suffix) { - return string.Join("_", new[] { id, version.ToString(), suffix }.Where(x => !string.IsNullOrWhiteSpace(x))); + return StringExtensions.JoinNonEmpty("_", id, version.ToString(), suffix); } } } diff --git a/src/Squidex.Infrastructure.MongoDb/Assets/MongoGridFsAssetStore.cs b/src/Squidex.Infrastructure.MongoDb/Assets/MongoGridFsAssetStore.cs index e373715a2..6956bff4c 100644 --- a/src/Squidex.Infrastructure.MongoDb/Assets/MongoGridFsAssetStore.cs +++ b/src/Squidex.Infrastructure.MongoDb/Assets/MongoGridFsAssetStore.cs @@ -128,7 +128,7 @@ namespace Squidex.Infrastructure.Assets private static string GetFileName(string id, long version, string suffix) { - return string.Join("_", new[] { id, version.ToString(), suffix }.Where(x => !string.IsNullOrWhiteSpace(x))); + return StringExtensions.JoinNonEmpty("_", id, version.ToString(), suffix); } } } \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs b/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs index 3bcf6ef5e..3c00b6ea8 100644 --- a/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs +++ b/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs @@ -7,7 +7,6 @@ using System; using System.IO; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Squidex.Infrastructure.Log; @@ -159,7 +158,7 @@ namespace Squidex.Infrastructure.Assets private string GetPath(string id, long version, string suffix) { - return Path.Combine(directory.FullName, string.Join("_", new[] { id, version.ToString(), suffix }.Where(x => !string.IsNullOrWhiteSpace(x)))); + return Path.Combine(directory.FullName, StringExtensions.JoinNonEmpty("_", id, version.ToString(), suffix)); } } } diff --git a/src/Squidex.Infrastructure/StringExtensions.cs b/src/Squidex.Infrastructure/StringExtensions.cs index a1c3b100d..eb0c309bd 100644 --- a/src/Squidex.Infrastructure/StringExtensions.cs +++ b/src/Squidex.Infrastructure/StringExtensions.cs @@ -16,9 +16,11 @@ namespace Squidex.Infrastructure public static class StringExtensions { private const char NullChar = (char)0; + private static readonly Regex SlugRegex = new Regex("^[a-z0-9]+(\\-[a-z0-9]+)*$", RegexOptions.Compiled); private static readonly Regex EmailRegex = new Regex("^[a-zA-Z0-9.!#$%&’*+\\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:.[a-zA-Z0-9-]+)*$", RegexOptions.Compiled); private static readonly Regex PropertyNameRegex = new Regex("^[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*$", RegexOptions.Compiled); + private static readonly Dictionary LowerCaseDiacritics; private static readonly Dictionary Diacritics = new Dictionary { @@ -555,6 +557,8 @@ namespace Squidex.Infrastructure public static string BuildFullUrl(this string baseUrl, string path, bool trailingSlash = false) { + Guard.NotNull(path, nameof(path)); + var url = $"{baseUrl.TrimEnd('/')}/{path.Trim('/')}"; if (trailingSlash && @@ -567,5 +571,34 @@ namespace Squidex.Infrastructure return url; } + + public static string JoinNonEmpty(string separator, params string[] parts) + { + Guard.NotNull(separator, nameof(separator)); + + if (parts == null || parts.Length == 0) + { + return string.Empty; + } + + var sb = new StringBuilder(); + + for (var i = 0; i < parts.Length; i++) + { + var part = parts[i]; + + if (!string.IsNullOrWhiteSpace(part)) + { + sb.Append(part); + + if (i < parts.Length - 1) + { + sb.Append(separator); + } + } + } + + return sb.ToString(); + } } } diff --git a/src/Squidex/Config/Domain/InfrastructureServices.cs b/src/Squidex/Config/Domain/InfrastructureServices.cs index 7b2e5d335..310f33d2e 100644 --- a/src/Squidex/Config/Domain/InfrastructureServices.cs +++ b/src/Squidex/Config/Domain/InfrastructureServices.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; using Microsoft.AspNetCore.DataProtection.Repositories; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Infrastructure; @@ -16,7 +15,6 @@ using NodaTime; using Squidex.Areas.Api.Controllers.News.Service; using Squidex.Domain.Apps.Entities.Apps.Diagnostics; using Squidex.Domain.Users; -using Squidex.Infrastructure; using Squidex.Infrastructure.Caching; using Squidex.Infrastructure.Diagnostics; using Squidex.Infrastructure.Translations; diff --git a/src/Squidex/app/framework/angular/forms/control-errors.component.ts b/src/Squidex/app/framework/angular/forms/control-errors.component.ts index 0f8533abb..a23a2c19c 100644 --- a/src/Squidex/app/framework/angular/forms/control-errors.component.ts +++ b/src/Squidex/app/framework/angular/forms/control-errors.component.ts @@ -61,10 +61,6 @@ export class ControlErrorsComponent extends StatefulComponent implements public ngOnDestroy() { super.ngOnDestroy(); - this.unsubscribe(); - } - - private unsubscribe() { if (this.control && this.originalMarkAsTouched) { this.control['markAsTouched'] = this.originalMarkAsTouched; } @@ -90,7 +86,7 @@ export class ControlErrorsComponent extends StatefulComponent implements } if (this.control !== control) { - this.unsubscribe(); + this.unsubscribeAll(); this.control = control; diff --git a/src/Squidex/app/framework/angular/forms/iframe-editor.component.html b/src/Squidex/app/framework/angular/forms/iframe-editor.component.html index 41a07a468..19adae7f7 100644 --- a/src/Squidex/app/framework/angular/forms/iframe-editor.component.html +++ b/src/Squidex/app/framework/angular/forms/iframe-editor.component.html @@ -1 +1 @@ - + diff --git a/src/Squidex/app/framework/angular/forms/iframe-editor.component.ts b/src/Squidex/app/framework/angular/forms/iframe-editor.component.ts index 0a19695d5..f316e9ece 100644 --- a/src/Squidex/app/framework/angular/forms/iframe-editor.component.ts +++ b/src/Squidex/app/framework/angular/forms/iframe-editor.component.ts @@ -5,9 +5,8 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, OnInit, Renderer2, ViewChild } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, OnChanges, OnInit, Renderer2, ViewChild } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; -import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; import { ExternalControlComponent, Types } from '@app/framework/internal'; @@ -22,50 +21,42 @@ export const SQX_IFRAME_EDITOR_CONTROL_VALUE_ACCESSOR: any = { providers: [SQX_IFRAME_EDITOR_CONTROL_VALUE_ACCESSOR], changeDetection: ChangeDetectionStrategy.OnPush }) -export class IFrameEditorComponent extends ExternalControlComponent implements AfterViewInit, OnInit { +export class IFrameEditorComponent extends ExternalControlComponent implements OnChanges, OnInit { private value: any; private isDisabled = false; private isInitialized = false; - private plugin: HTMLIFrameElement; @ViewChild('iframe') public iframe: ElementRef; @Input() - public set url(value: string) { - this.sanitizedUrl = this.sanitizer.bypassSecurityTrustResourceUrl(value); - } - - public sanitizedUrl: SafeResourceUrl; + public url: string; constructor(changeDetector: ChangeDetectorRef, - private readonly sanitizer: DomSanitizer, private readonly renderer: Renderer2 ) { super(changeDetector); } - public ngAfterViewInit() { - this.plugin = this.iframe.nativeElement; + public ngOnChanges() { + this.iframe.nativeElement.src = this.url; } public ngOnInit(): void { this.own( this.renderer.listen('window', 'message', (event: MessageEvent) => { - if (event.source === this.plugin.contentWindow) { + if (event.source === this.iframe.nativeElement.contentWindow) { const { type } = event.data; if (type === 'started') { this.isInitialized = true; - if (this.plugin.contentWindow && Types.isFunction(this.plugin.contentWindow.postMessage)) { - this.plugin.contentWindow.postMessage({ type: 'disabled', isDisabled: this.isDisabled }, '*'); - this.plugin.contentWindow.postMessage({ type: 'valueChanged', value: this.value }, '*'); - } + this.sendMessage({ type: 'disabled', isDisabled: this.isDisabled }); + this.sendMessage({ type: 'valueChanged', value: this.value }); } else if (type === 'resize') { const { height } = event.data; - this.plugin.height = height + 'px'; + this.iframe.nativeElement.height = height + 'px'; } else if (type === 'valueChanged') { const { value } = event.data; @@ -84,16 +75,20 @@ export class IFrameEditorComponent extends ExternalControlComponent impleme public writeValue(obj: any) { this.value = obj; - if (this.isInitialized && this.plugin.contentWindow && Types.isFunction(this.plugin.contentWindow.postMessage)) { - this.plugin.contentWindow.postMessage({ type: 'valueChanged', value: this.value }, '*'); - } + this.sendMessage({ type: 'valueChanged', value: this.value }); } public setDisabledState(isDisabled: boolean): void { this.isDisabled = isDisabled; - if (this.isInitialized && this.plugin.contentWindow && Types.isFunction(this.plugin.contentWindow.postMessage)) { - this.plugin.contentWindow.postMessage({ type: 'disabled', isDisabled: this.isDisabled }, '*'); + this.sendMessage({ type: 'disabled', isDisabled: this.isDisabled }); + } + + private sendMessage(message: any) { + const iframe = this.iframe.nativeElement; + + if (this.isInitialized && iframe.contentWindow && Types.isFunction(iframe.contentWindow.postMessage)) { + iframe.contentWindow.postMessage(message, '*'); } } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/modals/modal-target.directive.ts b/src/Squidex/app/framework/angular/modals/modal-target.directive.ts index 69111ca4e..428ae47c4 100644 --- a/src/Squidex/app/framework/angular/modals/modal-target.directive.ts +++ b/src/Squidex/app/framework/angular/modals/modal-target.directive.ts @@ -20,7 +20,7 @@ export class ModalTargetDirective extends ResourceOwner implements AfterViewInit @Input('sqxModalTarget') public set target(element: any) { if (element !== this.targetElement) { - this.ngOnDestroy(); + this.unsubscribeAll(); this.targetElement = element; diff --git a/src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.ts b/src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.ts index 98fc8ee3f..5894307ea 100644 --- a/src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.ts +++ b/src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.ts @@ -100,12 +100,12 @@ export class OnboardingTooltipComponent extends StatefulComponent implements OnD public hideThis() { this.onboardingService.disable(this.helpId); - this.ngOnDestroy(); + this.unsubscribeAll(); } public hideAll() { this.onboardingService.disableAll(); - this.ngOnDestroy(); + this.unsubscribeAll(); } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/sorted.directive.ts b/src/Squidex/app/framework/angular/sorted.directive.ts index 0951fb933..4e15dc7e3 100644 --- a/src/Squidex/app/framework/angular/sorted.directive.ts +++ b/src/Squidex/app/framework/angular/sorted.directive.ts @@ -5,14 +5,14 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Directive, ElementRef, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core'; +import { Directive, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; import * as Sortable from 'sortablejs'; @Directive({ selector: '[sqxSortModel]' }) -export class SortedDirective implements OnDestroy, OnInit, OnChanges { +export class SortedDirective implements OnDestroy, OnInit { private sortable: Sortable.Ref; @Input() @@ -29,13 +29,6 @@ export class SortedDirective implements OnDestroy, OnInit, OnChanges { ) { } - public ngOnChanges(changes: SimpleChanges) { - const sortModel = changes['sortModel'].currentValue; - if (sortModel) { - console.log(JSON.stringify(sortModel.map((x: any) => x.fileName))); - } - } - public ngOnDestroy() { if (this.sortable) { this.sortable.destroy(); @@ -48,7 +41,6 @@ export class SortedDirective implements OnDestroy, OnInit, OnChanges { animation: 150, onSort: (event: { oldIndex: number, newIndex: number }) => { - console.log('FOO'); if (this.sortModel && event.newIndex !== event.oldIndex) { const newModel = [...this.sortModel]; diff --git a/src/Squidex/app/framework/angular/stateful.component.ts b/src/Squidex/app/framework/angular/stateful.component.ts index b94de62c5..b3ba9f3c7 100644 --- a/src/Squidex/app/framework/angular/stateful.component.ts +++ b/src/Squidex/app/framework/angular/stateful.component.ts @@ -32,6 +32,10 @@ export class ResourceOwner implements OnDestroy { } public ngOnDestroy() { + this.unsubscribeAll(); + } + + public unsubscribeAll() { try { for (let subscription of this.subscriptions) { if (Types.isFunction(subscription)) { @@ -48,6 +52,7 @@ export class ResourceOwner implements OnDestroy { export abstract class StatefulComponent extends State implements OnDestroy { private readonly subscriptions = new ResourceOwner(); + private subscription: Subscription; constructor( private readonly changeDetector: ChangeDetectorRef, @@ -55,17 +60,23 @@ export abstract class StatefulComponent extends State implements OnD ) { super(state); - this.own( + this.subscription = this.changes.pipe(skip(1)).subscribe(() => { this.changeDetector.detectChanges(); - })); + }); } public ngOnDestroy() { - this.subscriptions.ngOnDestroy(); + this.subscription.unsubscribe(); + + this.unsubscribeAll(); + } + + protected unsubscribeAll() { + this.subscriptions.unsubscribeAll(); } - public detectChanges() { + protected detectChanges() { this.changeDetector.detectChanges(); } diff --git a/tests/Squidex.Infrastructure.Tests/Assets/AssetStoreTests.cs b/tests/Squidex.Infrastructure.Tests/Assets/AssetStoreTests.cs index 409ce87d1..13c37a6a9 100644 --- a/tests/Squidex.Infrastructure.Tests/Assets/AssetStoreTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Assets/AssetStoreTests.cs @@ -23,7 +23,10 @@ namespace Squidex.Infrastructure.Assets { sut = new Lazy(CreateStore); - ((IInitializable)Sut).InitializeAsync().Wait(); + if (Sut is IInitializable initializable) + { + initializable.InitializeAsync().Wait(); + } } protected T Sut