diff --git a/Nuget.config b/Nuget.config deleted file mode 100644 index 8c24b91a1..000000000 --- a/Nuget.config +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/extensions/Squidex.Extensions/Squidex.Extensions.csproj b/extensions/Squidex.Extensions/Squidex.Extensions.csproj index 5e0af3773..3edb1180c 100644 --- a/extensions/Squidex.Extensions/Squidex.Extensions.csproj +++ b/extensions/Squidex.Extensions/Squidex.Extensions.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj b/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj index bd47258f1..fda492bdf 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj +++ b/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj @@ -8,11 +8,11 @@ True - + all runtime; build; native; contentfiles; analyzers - + diff --git a/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj b/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj index f366ea646..4a1d40b7e 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj +++ b/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj @@ -17,7 +17,7 @@ - + diff --git a/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj b/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj index a3d6da666..f3c64022d 100644 --- a/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj +++ b/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj @@ -14,12 +14,12 @@ - - + + all runtime; build; native; contentfiles; analyzers - + diff --git a/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj b/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj index 8753ca0ef..56fc1237c 100644 --- a/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj +++ b/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj @@ -19,7 +19,7 @@ - + ..\..\Squidex.ruleset diff --git a/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj b/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj index 8a4d33405..539128d44 100644 --- a/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj +++ b/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj @@ -17,7 +17,7 @@ - + ..\..\Squidex.ruleset diff --git a/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj b/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj index e39741428..11d4cf7ba 100644 --- a/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj +++ b/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj @@ -11,12 +11,12 @@ - + all runtime; build; native; contentfiles; analyzers - - + + @@ -25,7 +25,7 @@ - + diff --git a/src/Squidex/Squidex.csproj b/src/Squidex/Squidex.csproj index c6c9c710c..536d85ab3 100644 --- a/src/Squidex/Squidex.csproj +++ b/src/Squidex/Squidex.csproj @@ -54,36 +54,36 @@ - + - + - + - - - - + + + + - + - + - + diff --git a/src/Squidex/app/features/administration/guards/user-must-exist.guard.ts b/src/Squidex/app/features/administration/guards/user-must-exist.guard.ts index db67514f9..63fc2bba1 100644 --- a/src/Squidex/app/features/administration/guards/user-must-exist.guard.ts +++ b/src/Squidex/app/features/administration/guards/user-must-exist.guard.ts @@ -32,7 +32,7 @@ export class UserMustExistGuard implements CanActivate { this.router.navigate(['/404']); } }), - map(u => u !== null)); + map(u => !!u)); return result; } diff --git a/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts b/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts index 30161a18b..a8df38179 100644 --- a/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts +++ b/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts @@ -38,8 +38,7 @@ export class EventConsumersPageComponent implements OnDestroy, OnInit { this.eventConsumersState.load(false, true).pipe(onErrorResumeNext()).subscribe(); this.timerSubscription = - timer(2000, 2000).pipe( - switchMap(x => this.eventConsumersState.load(true, true)), onErrorResumeNext()) + timer(2000, 2000).pipe(switchMap(x => this.eventConsumersState.load(true, true)), onErrorResumeNext()) .subscribe(); } diff --git a/src/Squidex/app/features/administration/pages/restore/restore-page.component.ts b/src/Squidex/app/features/administration/pages/restore/restore-page.component.ts index fa8977ccf..32884a7d2 100644 --- a/src/Squidex/app/features/administration/pages/restore/restore-page.component.ts +++ b/src/Squidex/app/features/administration/pages/restore/restore-page.component.ts @@ -8,7 +8,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { FormBuilder } from '@angular/forms'; import { Subscription, timer } from 'rxjs'; -import { switchMap } from 'rxjs/operators'; +import { filter, switchMap } from 'rxjs/operators'; import { AuthService, @@ -43,11 +43,9 @@ export class RestorePageComponent implements OnDestroy, OnInit { public ngOnInit() { this.timerSubscription = - timer(0, 2000).pipe(switchMap(() => this.backupsService.getRestore())) + timer(0, 2000).pipe(switchMap(() => this.backupsService.getRestore()), filter(x => !!x)) .subscribe(dto => { - if (dto !== null) { - this.restoreJob = dto; - } + this.restoreJob = dto!; }); } diff --git a/src/Squidex/app/features/content/module.ts b/src/Squidex/app/features/content/module.ts index 832b7c161..f08988fc9 100644 --- a/src/Squidex/app/features/content/module.ts +++ b/src/Squidex/app/features/content/module.ts @@ -15,6 +15,7 @@ import { ContentMustExistGuard, LoadLanguagesGuard, SchemaMustExistPublishedGuard, + SchemaMustNotBeSingletonGuard, SqxFrameworkModule, SqxSharedModule, UnsetContentGuard @@ -52,12 +53,13 @@ const routes: Routes = [ { path: '', component: ContentsPageComponent, + canActivate: [SchemaMustNotBeSingletonGuard], canDeactivate: [CanDeactivateGuard] }, { path: 'new', component: ContentPageComponent, - canActivate: [UnsetContentGuard], + canActivate: [SchemaMustNotBeSingletonGuard, UnsetContentGuard], canDeactivate: [CanDeactivateGuard] }, { diff --git a/src/Squidex/app/features/content/pages/content/content-field.component.html b/src/Squidex/app/features/content/pages/content/content-field.component.html index 3b7f60e12..b4ea1e648 100644 --- a/src/Squidex/app/features/content/pages/content/content-field.component.html +++ b/src/Squidex/app/features/content/pages/content/content-field.component.html @@ -1,4 +1,4 @@ -
+
; + public ngOnChanges(changes: SimpleChanges) { if (this.field.isLocalizable) { this.selectedFormControl = this.fieldForm.controls[this.language.iso2Code]; @@ -55,6 +60,10 @@ export class ContentFieldComponent implements OnChanges { this.selectedFormControl['_clearChangeFns'](); } } + + if (changes['fieldForm']) { + this.isInvalid = this.fieldForm.statusChanges.pipe(startWith(this.fieldForm.invalid), map(x => this.fieldForm.invalid)); + } } } diff --git a/src/Squidex/app/features/content/pages/content/content-history.component.html b/src/Squidex/app/features/content/pages/content/content-history.component.html index f9026f3dc..a3d040461 100644 --- a/src/Squidex/app/features/content/pages/content/content-history.component.html +++ b/src/Squidex/app/features/content/pages/content/content-history.component.html @@ -4,13 +4,14 @@ -
+
- {{event.actor | sqxUserNameRef:null}} + {{event.actor | sqxUserNameRef:null}} +
{{event.created | sqxFromNow}}
diff --git a/src/Squidex/app/features/content/pages/content/content-history.component.ts b/src/Squidex/app/features/content/pages/content/content-history.component.ts index 09763dc08..ad7718da9 100644 --- a/src/Squidex/app/features/content/pages/content/content-history.component.ts +++ b/src/Squidex/app/features/content/pages/content/content-history.component.ts @@ -13,12 +13,10 @@ import { delay, switchMap } from 'rxjs/operators'; import { allParams, AppsState, - formatHistoryMessage, HistoryChannelUpdated, HistoryEventDto, HistoryService, MessageBus, - UsersProviderService, Version } from '@app/shared'; @@ -59,8 +57,7 @@ export class ContentHistoryComponent { private readonly appsState: AppsState, private readonly historyService: HistoryService, private readonly messageBus: MessageBus, - private readonly route: ActivatedRoute, - private readonly users: UsersProviderService + private readonly route: ActivatedRoute ) { } @@ -68,7 +65,7 @@ export class ContentHistoryComponent { this.messageBus.emit(new ContentVersionSelected(new Version(version.toString()))); } - public format(message: string): Observable { - return formatHistoryMessage(message, this.users); + public trackByEvent(index: number, event: HistoryEventDto) { + return event.eventId; } } \ No newline at end of file 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 d8466fdcd..53b9f47b9 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 @@ -8,7 +8,7 @@ import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { Observable, of, Subscription } from 'rxjs'; -import { filter, map, onErrorResumeNext, switchMap } from 'rxjs/operators'; +import { filter, onErrorResumeNext, switchMap } from 'rxjs/operators'; import { ContentVersionSelected } from './../messages'; @@ -88,19 +88,19 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy, }); this.selectedSchemaSubscription = - this.schemasState.selectedSchema.pipe(filter(s => !!s), map(s => s!)) + this.schemasState.selectedSchema.pipe(filter(s => !!s)) .subscribe(schema => { - this.schema = schema; + this.schema = schema!; this.contentForm = new EditContentForm(this.schema, this.languages); }); this.contentSubscription = - this.contentsState.selectedContent.pipe(filter(c => !!c), map(c => c!)) + this.contentsState.selectedContent.pipe(filter(c => !!c)) .subscribe(content => { - this.content = content; + this.content = content!; - this.loadContent(content.dataDraft); + this.loadContent(this.content.dataDraft); }); this.contentVersionSelectedSubscription = diff --git a/src/Squidex/app/features/content/pages/contents/contents-page.component.html b/src/Squidex/app/features/content/pages/contents/contents-page.component.html index 0434c823e..74a81a361 100644 --- a/src/Squidex/app/features/content/pages/contents/contents-page.component.html +++ b/src/Squidex/app/features/content/pages/contents/contents-page.component.html @@ -73,19 +73,19 @@
{{selectionCount}} items selected:   - - - - 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 5f68b0ab2..be82c3a40 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 @@ -6,8 +6,9 @@ */ import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { Router } from '@angular/router'; import { Subscription } from 'rxjs'; -import { onErrorResumeNext, switchMap, tap } from 'rxjs/operators'; +import { filter, onErrorResumeNext, switchMap, takeUntil, tap } from 'rxjs/operators'; import { AppLanguageDto, @@ -17,6 +18,7 @@ import { ImmutableArray, LanguagesState, ModalModel, + navigatedToOtherComponent, Queries, SchemaDetailsDto, SchemasState, @@ -59,6 +61,7 @@ export class ContentsPageComponent implements OnDestroy, OnInit { public readonly contentsState: ContentsState, private readonly languagesState: LanguagesState, private readonly schemasState: SchemasState, + private readonly router: Router, private readonly uiState: UIState ) { } @@ -70,8 +73,10 @@ export class ContentsPageComponent implements OnDestroy, OnInit { } public ngOnInit() { + const routeChanged = this.router.events.pipe(filter(navigatedToOtherComponent(this.router))); + this.selectedSchemaSubscription = - this.schemasState.selectedSchema + this.schemasState.selectedSchema.pipe(takeUntil(routeChanged)) .subscribe(schema => { this.resetSelection(); @@ -82,7 +87,7 @@ export class ContentsPageComponent implements OnDestroy, OnInit { }); this.contentsSubscription = - this.contentsState.contents + this.contentsState.contents.pipe(takeUntil(routeChanged)) .subscribe(() => { this.updateSelectionSummary(); }); @@ -100,7 +105,7 @@ export class ContentsPageComponent implements OnDestroy, OnInit { } public deleteSelected() { - this.contentsState.deleteMany(this.select()).pipe(onErrorResumeNext()).subscribe(); + this.contentsState.deleteMany(this.selectItems()).pipe(onErrorResumeNext()).subscribe(); } public delete(content: ContentDto) { @@ -112,7 +117,7 @@ export class ContentsPageComponent implements OnDestroy, OnInit { } public publishSelected() { - this.changeContentItems(this.select(c => c.status !== 'Published'), 'Publish'); + this.changeContentItems(this.selectItems(c => c.status !== 'Published'), 'Publish'); } public unpublish(content: ContentDto) { @@ -120,7 +125,7 @@ export class ContentsPageComponent implements OnDestroy, OnInit { } public unpublishSelected() { - this.changeContentItems(this.select(c => c.status === 'Published'), 'Unpublish'); + this.changeContentItems(this.selectItems(c => c.status === 'Published'), 'Unpublish'); } public archive(content: ContentDto) { @@ -128,15 +133,15 @@ export class ContentsPageComponent implements OnDestroy, OnInit { } public archiveSelected() { - this.changeContentItems(this.select(), 'Archive'); + this.changeContentItems(this.selectItems(), 'Archive'); } public restore(content: ContentDto) { this.changeContentItems([content], 'Restore'); } - public restoreSelected(scheduled: boolean) { - this.changeContentItems(this.select(), 'Restore'); + public restoreSelected() { + this.changeContentItems(this.selectItems(), 'Restore'); } public clone(content: ContentDto) { @@ -144,7 +149,7 @@ export class ContentsPageComponent implements OnDestroy, OnInit { } public isSelectedQuery(query: string) { - return query === this.contentsState.snapshot.contentsQuery || (!query && !this.contentsState.contentsQuery); + return query === this.contentsState.snapshot.contentsQuery || (!query && !this.contentsState.snapshot.contentsQuery); } private changeContentItems(contents: ContentDto[], action: string) { @@ -185,12 +190,16 @@ export class ContentsPageComponent implements OnDestroy, OnInit { this.contentsState.search(query).pipe(onErrorResumeNext()).subscribe(); } + public selectLanguage(language: AppLanguageDto) { + this.language = language; + } + public isItemSelected(content: ContentDto): boolean { return !!this.selectedItems[content.id]; } - public selectLanguage(language: AppLanguageDto) { - this.language = language; + private selectItems(predicate?: (content: ContentDto) => boolean) { + return this.contentsState.snapshot.contents.values.filter(c => this.selectedItems[c.id] && (!predicate || predicate(c))); } public selectItem(content: ContentDto, isSelected: boolean) { @@ -199,6 +208,12 @@ export class ContentsPageComponent implements OnDestroy, OnInit { this.updateSelectionSummary(); } + private resetSelection() { + this.selectedItems = {}; + + this.updateSelectionSummary(); + } + public selectAll(isSelected: boolean) { this.selectedItems = {}; @@ -215,16 +230,6 @@ export class ContentsPageComponent implements OnDestroy, OnInit { return content.id; } - private select(predicate?: (content: ContentDto) => boolean) { - return this.contentsState.snapshot.contents.values.filter(c => this.selectedItems[c.id] && (!predicate || predicate(c))); - } - - private resetSelection() { - this.selectedItems = {}; - - this.updateSelectionSummary(); - } - private updateSelectionSummary() { this.isAllSelected = this.contentsState.snapshot.contents.length > 0; diff --git a/src/Squidex/app/features/content/pages/schemas/schemas-page.component.html b/src/Squidex/app/features/content/pages/schemas/schemas-page.component.html index 1903873f2..a270463cd 100644 --- a/src/Squidex/app/features/content/pages/schemas/schemas-page.component.html +++ b/src/Squidex/app/features/content/pages/schemas/schemas-page.component.html @@ -21,6 +21,7 @@ [name]="category" [schemas]="schemas" [schemasFilter]="schemasFilter.valueChanges | async" + [routeSingletonToContent]="true" [isReadonly]="true"> diff --git a/src/Squidex/app/features/content/shared/assets-editor.component.ts b/src/Squidex/app/features/content/shared/assets-editor.component.ts index bcfeaa2e7..b2ff9fa2f 100644 --- a/src/Squidex/app/features/content/shared/assets-editor.component.ts +++ b/src/Squidex/app/features/content/shared/assets-editor.component.ts @@ -7,7 +7,7 @@ // tslint:disable:prefer-for-of -import { Component, forwardRef } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { @@ -28,9 +28,8 @@ export const SQX_ASSETS_EDITOR_CONTROL_VALUE_ACCESSOR: any = { selector: 'sqx-assets-editor', styleUrls: ['./assets-editor.component.scss'], templateUrl: './assets-editor.component.html', - providers: [ - SQX_ASSETS_EDITOR_CONTROL_VALUE_ACCESSOR - ] + providers: [SQX_ASSETS_EDITOR_CONTROL_VALUE_ACCESSOR], + changeDetection: ChangeDetectionStrategy.OnPush }) export class AssetsEditorComponent implements ControlValueAccessor { private callChange = (v: any) => { /* NOOP */ }; @@ -47,6 +46,7 @@ export class AssetsEditorComponent implements ControlValueAccessor { constructor( private readonly appsState: AppsState, private readonly assetsService: AssetsService, + private readonly changeDetector: ChangeDetectorRef, private readonly localStore: LocalStoreService ) { this.isListView = this.localStore.get('assetView') === 'List'; @@ -131,6 +131,8 @@ export class AssetsEditorComponent implements ControlValueAccessor { this.callTouched(); this.callChange(ids); + + this.changeDetector.detectChanges(); } public sort(assets: AssetDto[]) { diff --git a/src/Squidex/app/features/content/shared/content-item.component.ts b/src/Squidex/app/features/content/shared/content-item.component.ts index 567915cd4..697f9044e 100644 --- a/src/Squidex/app/features/content/shared/content-item.component.ts +++ b/src/Squidex/app/features/content/shared/content-item.component.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; +import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; import { AppLanguageDto, @@ -29,7 +29,8 @@ import { templateUrl: './content-item.component.html', animations: [ fadeAnimation - ] + ], + changeDetection: ChangeDetectionStrategy.OnPush }) export class ContentItemComponent implements OnChanges { @Output() diff --git a/src/Squidex/app/features/content/shared/contents-selector.component.html b/src/Squidex/app/features/content/shared/contents-selector.component.html index 03c7a7934..c38612c34 100644 --- a/src/Squidex/app/features/content/shared/contents-selector.component.html +++ b/src/Squidex/app/features/content/shared/contents-selector.component.html @@ -16,10 +16,10 @@ (queryChanged)="search($event)" expandable="true"> - - - - +
+ +
+
diff --git a/src/Squidex/app/features/content/shared/references-editor.component.ts b/src/Squidex/app/features/content/shared/references-editor.component.ts index 5d4542a10..f0defad85 100644 --- a/src/Squidex/app/features/content/shared/references-editor.component.ts +++ b/src/Squidex/app/features/content/shared/references-editor.component.ts @@ -7,7 +7,7 @@ // tslint:disable:prefer-for-of -import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { @@ -31,9 +31,8 @@ export const SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR: any = { selector: 'sqx-references-editor', styleUrls: ['./references-editor.component.scss'], templateUrl: './references-editor.component.html', - providers: [ - SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR - ] + providers: [SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR], + changeDetection: ChangeDetectionStrategy.OnPush }) export class ReferencesEditorComponent implements ControlValueAccessor, OnInit { private callChange = (v: any) => { /* NOOP */ }; @@ -59,6 +58,7 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit { constructor( private readonly appsState: AppsState, + private readonly changeDetector: ChangeDetectorRef, private readonly contentsService: ContentsService, private readonly schemasService: SchemasService ) { @@ -73,8 +73,12 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit { this.schemasService.getSchema(this.appsState.appName, this.schemaId) .subscribe(dto => { this.schema = dto; - }, error => { + + this.changeDetector.detectChanges(); + }, () => { this.isInvalidSchema = true; + + this.changeDetector.detectChanges(); }); } @@ -90,8 +94,12 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit { if (this.contentItems.length !== contentIds.length) { this.updateValue(); } + + this.changeDetector.detectChanges(); }, () => { this.contentItems = ImmutableArray.empty(); + + this.changeDetector.detectChanges(); }); } } else { @@ -148,5 +156,7 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit { this.callTouched(); this.callChange(ids); + + this.changeDetector.detectChanges(); } } \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts b/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts index 0405d9426..ba2eaad1d 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 @@ -82,7 +82,7 @@ export class ContentChangedTriggerComponent implements OnInit { } else { return null; } - }).filter(s => s !== null).map(s => s!)).sortByStringAsc(s => s.schema.name); + }).filter(s => !!s).map(s => s!)).sortByStringAsc(s => s.schema.name); this.schemasToAdd = this.schemas.filter(schema => 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 92ab4699e..81c86168f 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 @@ -10,7 +10,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { Subscription } from 'rxjs'; -import { filter, map, onErrorResumeNext } from 'rxjs/operators'; +import { filter, onErrorResumeNext } from 'rxjs/operators'; import { AppsState, @@ -73,9 +73,9 @@ export class SchemaPageComponent implements OnDestroy, OnInit { this.patternsState.load().pipe(onErrorResumeNext()).subscribe(); this.selectedSchemaSubscription = - this.schemasState.selectedSchema.pipe(filter(s => !!s), map(s => s!)) + this.schemasState.selectedSchema.pipe(filter(s => !!s)) .subscribe(schema => { - this.schema = schema; + this.schema = schema!; this.export(); }); diff --git a/src/Squidex/app/framework/angular/forms/autocomplete.component.ts b/src/Squidex/app/framework/angular/forms/autocomplete.component.ts index a8666a9cf..bacb8a845 100644 --- a/src/Squidex/app/framework/angular/forms/autocomplete.component.ts +++ b/src/Squidex/app/framework/angular/forms/autocomplete.component.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, ContentChild, forwardRef, Input, OnDestroy, OnInit, TemplateRef } from '@angular/core'; +import { ChangeDetectionStrategy, Component, ContentChild, forwardRef, Input, OnDestroy, OnInit, TemplateRef } from '@angular/core'; import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; import { Observable, of, Subscription } from 'rxjs'; import { catchError, debounceTime, distinctUntilChanged, filter, map, switchMap, tap } from 'rxjs/operators'; @@ -27,7 +27,8 @@ export const SQX_AUTOCOMPLETE_CONTROL_VALUE_ACCESSOR: any = { selector: 'sqx-autocomplete', styleUrls: ['./autocomplete.component.scss'], templateUrl: './autocomplete.component.html', - providers: [SQX_AUTOCOMPLETE_CONTROL_VALUE_ACCESSOR] + providers: [SQX_AUTOCOMPLETE_CONTROL_VALUE_ACCESSOR], + changeDetection: ChangeDetectionStrategy.OnPush }) export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, OnInit { private subscription: Subscription; diff --git a/src/Squidex/app/framework/angular/forms/date-time-editor.component.ts b/src/Squidex/app/framework/angular/forms/date-time-editor.component.ts index 6dbc72276..a98d02f29 100644 --- a/src/Squidex/app/framework/angular/forms/date-time-editor.component.ts +++ b/src/Squidex/app/framework/angular/forms/date-time-editor.component.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { AfterViewInit, Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; import * as moment from 'moment'; import { Subscription } from 'rxjs'; @@ -22,7 +22,8 @@ export const SQX_DATE_TIME_EDITOR_CONTROL_VALUE_ACCESSOR: any = { selector: 'sqx-date-time-editor', styleUrls: ['./date-time-editor.component.scss'], templateUrl: './date-time-editor.component.html', - providers: [SQX_DATE_TIME_EDITOR_CONTROL_VALUE_ACCESSOR] + providers: [SQX_DATE_TIME_EDITOR_CONTROL_VALUE_ACCESSOR], + changeDetection: ChangeDetectionStrategy.OnPush }) export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy, OnInit, AfterViewInit { private timeSubscription: Subscription; @@ -52,7 +53,7 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy, } public get hasValue() { - return this.dateValue !== null; + return !!this.dateValue; } @ViewChild('dateInput') @@ -60,6 +61,11 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy, public isDisabled = false; + constructor( + private readonly changeDetector: ChangeDetectorRef + ) { + } + public ngOnDestroy() { this.dateSubscription.unsubscribe(); this.timeSubscription.unsubscribe(); @@ -136,6 +142,10 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy, this.updateValue(); this.touched(); + + if (false) { + this.changeDetector.detectChanges(); + } } }); diff --git a/src/Squidex/app/framework/angular/forms/dropdown.component.ts b/src/Squidex/app/framework/angular/forms/dropdown.component.ts index 4788bce5a..f5819e39c 100644 --- a/src/Squidex/app/framework/angular/forms/dropdown.component.ts +++ b/src/Squidex/app/framework/angular/forms/dropdown.component.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { AfterContentInit, Component, ContentChildren, forwardRef, Input, QueryList, TemplateRef } from '@angular/core'; +import { AfterContentInit, ChangeDetectionStrategy, Component, ContentChildren, forwardRef, Input, QueryList, TemplateRef } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; const KEY_ENTER = 13; @@ -23,7 +23,8 @@ export const SQX_DROPDOWN_CONTROL_VALUE_ACCESSOR: any = { selector: 'sqx-dropdown', styleUrls: ['./dropdown.component.scss'], templateUrl: './dropdown.component.html', - providers: [SQX_DROPDOWN_CONTROL_VALUE_ACCESSOR] + providers: [SQX_DROPDOWN_CONTROL_VALUE_ACCESSOR], + changeDetection: ChangeDetectionStrategy.OnPush }) export class DropdownComponent implements AfterContentInit, ControlValueAccessor { private callChange = (v: any) => { /* NOOP */ }; 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 44f811b58..41a07a468 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 867e00c6d..374da7303 100644 --- a/src/Squidex/app/framework/angular/forms/iframe-editor.component.ts +++ b/src/Squidex/app/framework/angular/forms/iframe-editor.component.ts @@ -7,7 +7,7 @@ import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, Renderer2, ViewChild } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { DomSanitizer } from '@angular/platform-browser'; +import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; import { Types } from '@app/framework/internal'; @@ -19,8 +19,8 @@ export const SQX_IFRAME_EDITOR_CONTROL_VALUE_ACCESSOR: any = { selector: 'sqx-iframe-editor', styleUrls: ['./iframe-editor.component.scss'], templateUrl: './iframe-editor.component.html', - changeDetection: ChangeDetectionStrategy.OnPush, - providers: [SQX_IFRAME_EDITOR_CONTROL_VALUE_ACCESSOR] + providers: [SQX_IFRAME_EDITOR_CONTROL_VALUE_ACCESSOR], + changeDetection: ChangeDetectionStrategy.OnPush }) export class IFrameEditorComponent implements ControlValueAccessor, AfterViewInit, OnInit, OnDestroy { private windowMessageListener: Function; @@ -35,7 +35,11 @@ export class IFrameEditorComponent implements ControlValueAccessor, AfterViewIni public iframe: ElementRef; @Input() - public url: string; + public set url(value: string) { + this.sanitizedUrl = this.sanitizer.bypassSecurityTrustResourceUrl(value); + } + + public sanitizedUrl: SafeResourceUrl; constructor( private readonly sanitizer: DomSanitizer, @@ -83,10 +87,6 @@ export class IFrameEditorComponent implements ControlValueAccessor, AfterViewIni }); } - public sanitizedUrl() { - return this.sanitizer.bypassSecurityTrustResourceUrl(this.url); - } - public writeValue(obj: any) { this.value = obj; diff --git a/src/Squidex/app/framework/angular/forms/jscript-editor.component.ts b/src/Squidex/app/framework/angular/forms/jscript-editor.component.ts index ba54c6150..9675afa61 100644 --- a/src/Squidex/app/framework/angular/forms/jscript-editor.component.ts +++ b/src/Squidex/app/framework/angular/forms/jscript-editor.component.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { AfterViewInit, Component, ElementRef, forwardRef, ViewChild } from '@angular/core'; +import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, forwardRef, ViewChild } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { Subject } from 'rxjs'; import { debounceTime } from 'rxjs/operators'; @@ -22,7 +22,8 @@ export const SQX_JSCRIPT_EDITOR_CONTROL_VALUE_ACCESSOR: any = { selector: 'sqx-jscript-editor', styleUrls: ['./jscript-editor.component.scss'], templateUrl: './jscript-editor.component.html', - providers: [SQX_JSCRIPT_EDITOR_CONTROL_VALUE_ACCESSOR] + providers: [SQX_JSCRIPT_EDITOR_CONTROL_VALUE_ACCESSOR], + changeDetection: ChangeDetectionStrategy.OnPush }) export class JscriptEditorComponent implements ControlValueAccessor, AfterViewInit { private callChange = (v: any) => { /* NOOP */ }; diff --git a/src/Squidex/app/framework/angular/forms/json-editor.component.ts b/src/Squidex/app/framework/angular/forms/json-editor.component.ts index 3a97d3eb0..35f84d0b5 100644 --- a/src/Squidex/app/framework/angular/forms/json-editor.component.ts +++ b/src/Squidex/app/framework/angular/forms/json-editor.component.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { AfterViewInit, Component, ElementRef, forwardRef, ViewChild } from '@angular/core'; +import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, forwardRef, ViewChild } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { Subject } from 'rxjs'; import { debounceTime } from 'rxjs/operators'; @@ -22,7 +22,8 @@ export const SQX_JSON_EDITOR_CONTROL_VALUE_ACCESSOR: any = { selector: 'sqx-json-editor', styleUrls: ['./json-editor.component.scss'], templateUrl: './json-editor.component.html', - providers: [SQX_JSON_EDITOR_CONTROL_VALUE_ACCESSOR] + providers: [SQX_JSON_EDITOR_CONTROL_VALUE_ACCESSOR], + changeDetection: ChangeDetectionStrategy.OnPush }) export class JsonEditorComponent implements ControlValueAccessor, AfterViewInit { private callChange = (v: any) => { /* NOOP */ }; diff --git a/src/Squidex/app/framework/angular/forms/progress-bar.component.ts b/src/Squidex/app/framework/angular/forms/progress-bar.component.ts index 31d6f3dee..843574f04 100644 --- a/src/Squidex/app/framework/angular/forms/progress-bar.component.ts +++ b/src/Squidex/app/framework/angular/forms/progress-bar.component.ts @@ -5,13 +5,14 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, ElementRef, Input, OnChanges, OnInit, Renderer2, SimpleChanges } from '@angular/core'; +import { ChangeDetectionStrategy, Component, ElementRef, Input, OnChanges, OnInit, Renderer2, SimpleChanges } from '@angular/core'; const ProgressBar = require('progressbar.js'); @Component({ selector: 'sqx-progress-bar', - template: '' + template: '', + changeDetection: ChangeDetectionStrategy.OnPush }) export class ProgressBarComponent implements OnChanges, OnInit { private progressBar: any; @@ -31,6 +32,9 @@ export class ProgressBarComponent implements OnChanges, OnInit { @Input() public strokeWidth = 4; + @Input() + public showText = true; + @Input() public value = 0; @@ -45,7 +49,8 @@ export class ProgressBarComponent implements OnChanges, OnInit { color: this.color, trailColor: this.trailColor, trailWidth: this.trailWidth, - strokeWidth: this.strokeWidth + strokeWidth: this.strokeWidth, + svgStyle: { width: '100%', height: '100%' } }; this.renderer.setStyle(this.element.nativeElement, 'display', 'block'); @@ -70,7 +75,7 @@ export class ProgressBarComponent implements OnChanges, OnInit { this.progressBar.animate(value / 100); - if (value > 0) { + if (value > 0 && this.showText) { this.progressBar.setText(Math.round(value) + '%'); } } diff --git a/src/Squidex/app/framework/angular/forms/slider.component.ts b/src/Squidex/app/framework/angular/forms/slider.component.ts index 7e03256aa..f7baeb850 100644 --- a/src/Squidex/app/framework/angular/forms/slider.component.ts +++ b/src/Squidex/app/framework/angular/forms/slider.component.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, ElementRef, forwardRef, Input, Renderer2, ViewChild } from '@angular/core'; +import { ChangeDetectionStrategy, Component, ElementRef, forwardRef, Input, Renderer2, ViewChild } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { Types } from '@app/framework/internal'; @@ -18,7 +18,8 @@ export const SQX_SLIDER_CONTROL_VALUE_ACCESSOR: any = { selector: 'sqx-slider', styleUrls: ['./slider.component.scss'], templateUrl: './slider.component.html', - providers: [SQX_SLIDER_CONTROL_VALUE_ACCESSOR] + providers: [SQX_SLIDER_CONTROL_VALUE_ACCESSOR], + changeDetection: ChangeDetectionStrategy.OnPush }) export class SliderComponent implements ControlValueAccessor { private callChange = (v: any) => { /* NOOP */ }; diff --git a/src/Squidex/app/framework/angular/forms/stars.component.ts b/src/Squidex/app/framework/angular/forms/stars.component.ts index 0b3b0c728..dd30c81ad 100644 --- a/src/Squidex/app/framework/angular/forms/stars.component.ts +++ b/src/Squidex/app/framework/angular/forms/stars.component.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, forwardRef, Input } from '@angular/core'; +import { ChangeDetectionStrategy, Component, forwardRef, Input } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { Types } from '@app/framework/internal'; @@ -18,7 +18,8 @@ export const SQX_STARS_CONTROL_VALUE_ACCESSOR: any = { selector: 'sqx-stars', styleUrls: ['./stars.component.scss'], templateUrl: './stars.component.html', - providers: [SQX_STARS_CONTROL_VALUE_ACCESSOR] + providers: [SQX_STARS_CONTROL_VALUE_ACCESSOR], + changeDetection: ChangeDetectionStrategy.OnPush }) export class StarsComponent implements ControlValueAccessor { private callChange = (v: any) => { /* NOOP */ }; @@ -88,7 +89,7 @@ export class StarsComponent implements ControlValueAccessor { return false; } - if (this.value !== null) { + if (this.value) { this.value = null; this.stars = 0; diff --git a/src/Squidex/app/framework/angular/forms/tag-editor.component.html b/src/Squidex/app/framework/angular/forms/tag-editor.component.html index 3de720b30..0c3dbe926 100644 --- a/src/Squidex/app/framework/angular/forms/tag-editor.component.html +++ b/src/Squidex/app/framework/angular/forms/tag-editor.component.html @@ -14,7 +14,6 @@ [formControl]="addInput" [attr.name]="inputName" [attr.placeholder]="placeholder" - [disabled]="addInput.disabled" autocomplete="off" autocorrect="off" autocapitalize="off" diff --git a/src/Squidex/app/framework/angular/forms/tag-editor.component.ts b/src/Squidex/app/framework/angular/forms/tag-editor.component.ts index 9a7422295..98d2068d6 100644 --- a/src/Squidex/app/framework/angular/forms/tag-editor.component.ts +++ b/src/Squidex/app/framework/angular/forms/tag-editor.component.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { AfterViewInit, Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; import { Subscription } from 'rxjs'; import { distinctUntilChanged, map, tap } from 'rxjs/operators'; @@ -71,11 +71,16 @@ export const SQX_TAG_EDITOR_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => TagEditorComponent), multi: true }; +const CACHED_SIZES: { [key: string]: number } = {}; + +let CACHED_FONT: string; + @Component({ selector: 'sqx-tag-editor', styleUrls: ['./tag-editor.component.scss'], templateUrl: './tag-editor.component.html', - providers: [SQX_TAG_EDITOR_CONTROL_VALUE_ACCESSOR] + providers: [SQX_TAG_EDITOR_CONTROL_VALUE_ACCESSOR], + changeDetection: ChangeDetectionStrategy.OnPush }) export class TagEditorComponent implements AfterViewInit, ControlValueAccessor, OnDestroy, OnInit { private subscription: Subscription; @@ -129,6 +134,12 @@ export class TagEditorComponent implements AfterViewInit, ControlValueAccessor, } public ngAfterViewInit() { + if (!CACHED_FONT) { + const style = window.getComputedStyle(this.inputElement.nativeElement); + + CACHED_FONT = `${style.getPropertyValue('font-size')} ${style.getPropertyValue('font-family')}`; + } + this.resetSize(); } @@ -204,7 +215,9 @@ export class TagEditorComponent implements AfterViewInit, ControlValueAccessor, } public resetSize() { - const style = window.getComputedStyle(this.inputElement.nativeElement); + if (!CACHED_FONT) { + return; + } if (!canvas) { canvas = document.createElement('canvas'); @@ -214,20 +227,31 @@ export class TagEditorComponent implements AfterViewInit, ControlValueAccessor, const ctx = canvas.getContext('2d'); if (ctx) { - ctx.font = `${style.getPropertyValue('font-size')} ${style.getPropertyValue('font-family')}`; + ctx.font = CACHED_FONT; - const widthText = ctx.measureText(this.inputElement.nativeElement.value).width; - const widthPlaceholder = ctx.measureText(this.placeholder).width; + const text = this.inputElement.nativeElement.value; + const textKey = `${text}§${this.placeholder}§${ctx.font}`; - const width = Math.max(widthText, widthPlaceholder); + let width = CACHED_SIZES[textKey]; + + if (!width) { + const widthText = ctx.measureText(text).width; + const widthPlaceholder = ctx.measureText(this.placeholder).width; + + width = Math.max(widthText, widthPlaceholder); + + CACHED_SIZES[textKey] = width; + } this.inputElement.nativeElement.style.width = ((width + 5) + 'px'); } } - setTimeout(() => { - this.formElement.nativeElement.scrollLeft = this.formElement.nativeElement.scrollWidth; - }, 0); + if (this.singleLine) { + setTimeout(() => { + this.formElement.nativeElement.scrollLeft = this.formElement.nativeElement.scrollWidth; + }, 0); + } } public onKeyDown(event: KeyboardEvent) { diff --git a/src/Squidex/app/framework/angular/forms/toggle.component.ts b/src/Squidex/app/framework/angular/forms/toggle.component.ts index 270ef5eb8..2a7323fde 100644 --- a/src/Squidex/app/framework/angular/forms/toggle.component.ts +++ b/src/Squidex/app/framework/angular/forms/toggle.component.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, forwardRef } from '@angular/core'; +import { ChangeDetectionStrategy, Component, forwardRef } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { Types } from '@app/framework/internal'; @@ -18,7 +18,8 @@ export const SQX_TOGGLE_CONTROL_VALUE_ACCESSOR: any = { selector: 'sqx-toggle', styleUrls: ['./toggle.component.scss'], templateUrl: './toggle.component.html', - providers: [SQX_TOGGLE_CONTROL_VALUE_ACCESSOR] + providers: [SQX_TOGGLE_CONTROL_VALUE_ACCESSOR], + changeDetection: ChangeDetectionStrategy.OnPush }) export class ToggleComponent implements ControlValueAccessor { private callChange = (v: any) => { /* NOOP */ }; diff --git a/src/Squidex/app/framework/angular/http/loading.interceptor.ts b/src/Squidex/app/framework/angular/http/loading.interceptor.ts new file mode 100644 index 000000000..af6611f67 --- /dev/null +++ b/src/Squidex/app/framework/angular/http/loading.interceptor.ts @@ -0,0 +1,31 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; +import { Injectable} from '@angular/core'; +import { Observable } from 'rxjs'; +import { finalize } from 'rxjs/operators'; + +import { LoadingService, MathHelper } from './../../internal'; + +@Injectable() +export class LoadingInterceptor implements HttpInterceptor { + constructor( + private readonly loadingService: LoadingService + ) { + } + + public intercept(req: HttpRequest, next: HttpHandler): Observable> { + const id = MathHelper.guid(); + + this.loadingService.startLoading(id); + + return next.handle(req).pipe(finalize(() => { + this.loadingService.completeLoading(id); + })); + } +} \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/image-source.directive.ts b/src/Squidex/app/framework/angular/image-source.directive.ts index 2bc972977..c9406b524 100644 --- a/src/Squidex/app/framework/angular/image-source.directive.ts +++ b/src/Squidex/app/framework/angular/image-source.directive.ts @@ -9,6 +9,8 @@ import { AfterViewInit, Directive, ElementRef, HostListener, Input, OnChanges, O import { MathHelper } from './../utils/math-helper'; +const LAYOUT_CACHE: { [key: string]: { width: number, height: number } } = {}; + @Directive({ selector: '[sqxImageSource]' }) @@ -26,6 +28,9 @@ export class ImageSourceDirective implements OnChanges, OnDestroy, OnInit, After @Input() public retryCount = 10; + @Input() + public layoutKey: string; + @Input() public parent: any = null; @@ -76,7 +81,21 @@ export class ImageSourceDirective implements OnChanges, OnDestroy, OnInit, After } private resize() { - this.size = this.parent.getBoundingClientRect(); + let size: { width: number, height: number } = null!; + + if (this.layoutKey) { + size = LAYOUT_CACHE[this.layoutKey]; + } + + if (!size) { + size = { width: this.parent.offsetWidth, height: this.parent.offsetHeight }; + } + + this.size = size; + + if (this.layoutKey) { + LAYOUT_CACHE[this.layoutKey] = size; + } this.renderer.setStyle(this.element.nativeElement, 'display', 'inline-block'); this.renderer.setStyle(this.element.nativeElement, 'width', this.size.width + 'px'); @@ -96,7 +115,7 @@ export class ImageSourceDirective implements OnChanges, OnDestroy, OnInit, After if (w > 0 && h > 0) { let source = `${this.imageSource}&width=${w}&height=${h}&mode=Crop`; - if (this.loadQuery !== null) { + if (this.loadQuery) { source += `&q=${this.loadQuery}`; } diff --git a/src/Squidex/app/framework/angular/modals/dialog-renderer.component.ts b/src/Squidex/app/framework/angular/modals/dialog-renderer.component.ts index 2dabc425a..17c184f0c 100644 --- a/src/Squidex/app/framework/angular/modals/dialog-renderer.component.ts +++ b/src/Squidex/app/framework/angular/modals/dialog-renderer.component.ts @@ -5,7 +5,7 @@ * Copyright (c) Sebastian Stehle. All rights r vbeserved */ -import { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; import { Subscription } from 'rxjs'; import { @@ -22,7 +22,8 @@ import { templateUrl: './dialog-renderer.component.html', animations: [ fadeAnimation - ] + ], + changeDetection: ChangeDetectionStrategy.OnPush }) export class DialogRendererComponent implements OnDestroy, OnInit { private dialogSubscription: Subscription; @@ -38,6 +39,7 @@ export class DialogRendererComponent implements OnDestroy, OnInit { public position = 'bottomright'; constructor( + private readonly changeDetector: ChangeDetectorRef, private readonly dialogs: DialogService ) { } @@ -53,6 +55,8 @@ export class DialogRendererComponent implements OnDestroy, OnInit { this.dialogView.isOpen.subscribe(isOpen => { if (!isOpen) { this.cancel(); + + this.changeDetector.detectChanges(); } }); @@ -65,6 +69,8 @@ export class DialogRendererComponent implements OnDestroy, OnInit { this.close(notification); }, notification.displayTime); } + + this.changeDetector.detectChanges(); }); this.dialogsSubscription = @@ -74,6 +80,8 @@ export class DialogRendererComponent implements OnDestroy, OnInit { this.dialogRequest = request; this.dialogView.show(); + + this.changeDetector.detectChanges(); }); } @@ -94,6 +102,12 @@ export class DialogRendererComponent implements OnDestroy, OnInit { } public close(notification: Notification) { - this.notifications.splice(this.notifications.indexOf(notification), 1); + const index = this.notifications.indexOf(notification); + + if (index >= 0) { + this.notifications.splice(index, 1); + + this.changeDetector.detectChanges(); + } } } \ 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 3858649e1..8ecbcf9d0 100644 --- a/src/Squidex/app/framework/angular/modals/modal-target.directive.ts +++ b/src/Squidex/app/framework/angular/modals/modal-target.directive.ts @@ -91,8 +91,8 @@ export class ModalTargetDirective implements AfterViewInit, OnDestroy, OnInit { return; } - const viewportHeight = document.documentElement.clientHeight; - const viewportWidth = document.documentElement.clientWidth; + const viewportHeight = document.documentElement!.clientHeight; + const viewportWidth = document.documentElement!.clientWidth; const modalRef = this.element.nativeElement; const modalRect = this.element.nativeElement.getBoundingClientRect(); diff --git a/src/Squidex/app/framework/angular/modals/modal-view.directive.ts b/src/Squidex/app/framework/angular/modals/modal-view.directive.ts index 160488675..16a4834d3 100644 --- a/src/Squidex/app/framework/angular/modals/modal-view.directive.ts +++ b/src/Squidex/app/framework/angular/modals/modal-view.directive.ts @@ -71,7 +71,7 @@ export class ModalViewDirective implements OnChanges, OnDestroy { } private update(isOpen: boolean) { - if (isOpen === (this.renderedView !== null)) { + if (isOpen === (!!this.renderedView)) { return; } 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 b6cee0c64..9293b3b7e 100644 --- a/src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.ts +++ b/src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, Input, OnDestroy, OnInit, Renderer2 } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit, Renderer2 } from '@angular/core'; import { fadeAnimation, @@ -20,7 +20,8 @@ import { templateUrl: './onboarding-tooltip.component.html', animations: [ fadeAnimation - ] + ], + changeDetection: ChangeDetectionStrategy.OnPush }) export class OnboardingTooltipComponent implements OnDestroy, OnInit { private showTimer: any; @@ -42,6 +43,7 @@ export class OnboardingTooltipComponent implements OnDestroy, OnInit { public position = 'left'; constructor( + private readonly changeDetector: ChangeDetectorRef, private readonly onboardingService: OnboardingService, private readonly renderer: Renderer2 ) { @@ -73,6 +75,8 @@ export class OnboardingTooltipComponent implements OnDestroy, OnInit { if (this.isSameOrParent(fromPoint)) { this.tooltipModal.show(); + this.changeDetector.detectChanges(); + this.closeTimer = setTimeout(() => { this.hideThis(); }, 10000); diff --git a/src/Squidex/app/framework/angular/modals/root-view.component.ts b/src/Squidex/app/framework/angular/modals/root-view.component.ts index 4017109d9..2f3ec887c 100644 --- a/src/Squidex/app/framework/angular/modals/root-view.component.ts +++ b/src/Squidex/app/framework/angular/modals/root-view.component.ts @@ -5,12 +5,13 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, ViewChild, ViewContainerRef } from '@angular/core'; +import { ChangeDetectionStrategy, Component, ViewChild, ViewContainerRef } from '@angular/core'; @Component({ selector: 'sqx-root-view', styleUrls: ['./root-view.component.scss'], - templateUrl: './root-view.component.html' + templateUrl: './root-view.component.html', + changeDetection: ChangeDetectionStrategy.OnPush }) export class RootViewComponent { @ViewChild('element', { read: ViewContainerRef }) diff --git a/src/Squidex/app/framework/angular/modals/tooltip.component.ts b/src/Squidex/app/framework/angular/modals/tooltip.component.ts index 69759bbbe..c43a2352d 100644 --- a/src/Squidex/app/framework/angular/modals/tooltip.component.ts +++ b/src/Squidex/app/framework/angular/modals/tooltip.component.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit, Renderer2 } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit, Renderer2 } from '@angular/core'; import { ModalModel } from './../../utils/modal-view'; @@ -33,6 +33,7 @@ export class TooltipComponent implements OnDestroy, OnInit { public modal = new ModalModel(); constructor( + private readonly changeDetector: ChangeDetectorRef, private readonly renderer: Renderer2 ) { } @@ -52,6 +53,8 @@ export class TooltipComponent implements OnDestroy, OnInit { this.targetMouseEnterListener = this.renderer.listen(this.target, 'mouseenter', () => { this.modal.show(); + + this.changeDetector.detectChanges(); }); this.targetMouseLeaveListener = diff --git a/src/Squidex/app/framework/angular/panel-container.directive.ts b/src/Squidex/app/framework/angular/panel-container.directive.ts index 4ddd78943..fd4bac69a 100644 --- a/src/Squidex/app/framework/angular/panel-container.directive.ts +++ b/src/Squidex/app/framework/angular/panel-container.directive.ts @@ -14,6 +14,7 @@ import { PanelComponent } from './panel.component'; }) export class PanelContainerDirective implements AfterViewInit { private readonly panels: PanelComponent[] = []; + private isViewInit = false; private containerWidth = 0; constructor( @@ -27,14 +28,14 @@ export class PanelContainerDirective implements AfterViewInit { this.invalidate(true); } - public ngAfterViewInit() { - this.invalidate(true); - } - public push(panel: PanelComponent) { this.panels.push(panel); + } - this.invalidate(); + public ngAfterViewInit() { + this.isViewInit = true; + + this.invalidate(true); } public pop() { @@ -44,8 +45,12 @@ export class PanelContainerDirective implements AfterViewInit { } public invalidate(resize = false) { + if (!this.isViewInit) { + return; + } + if (resize) { - this.containerWidth = this.element.nativeElement.getBoundingClientRect().width; + this.containerWidth = this.element.nativeElement.offsetWidth; } const panels = this.panels; diff --git a/src/Squidex/app/framework/angular/panel.component.ts b/src/Squidex/app/framework/angular/panel.component.ts index 7c70ef8fc..e73ce66a3 100644 --- a/src/Squidex/app/framework/angular/panel.component.ts +++ b/src/Squidex/app/framework/angular/panel.component.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { AfterViewInit, Component, ElementRef, Input, OnDestroy, OnInit, Renderer2, ViewChild } from '@angular/core'; +import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, Input, OnDestroy, OnInit, Renderer2, ViewChild } from '@angular/core'; import { slideRightAnimation } from './animations'; @@ -17,7 +17,8 @@ import { PanelContainerDirective } from './panel-container.directive'; templateUrl: './panel.component.html', animations: [ slideRightAnimation - ] + ], + changeDetection: ChangeDetectionStrategy.OnPush }) export class PanelComponent implements AfterViewInit, OnDestroy, OnInit { private styleWidth: string; @@ -83,8 +84,7 @@ export class PanelComponent implements AfterViewInit, OnDestroy, OnInit { this.styleWidth = size; this.renderer.setStyle(this.panel.nativeElement, 'width', size); - - this.renderWidth = this.panel.nativeElement.getBoundingClientRect().width; + this.renderWidth = this.panel.nativeElement.offsetWidth; } } diff --git a/src/Squidex/app/framework/angular/routers/router-utils.ts b/src/Squidex/app/framework/angular/routers/router-utils.ts index 6dbfe0831..4dd16a7a8 100644 --- a/src/Squidex/app/framework/angular/routers/router-utils.ts +++ b/src/Squidex/app/framework/angular/routers/router-utils.ts @@ -5,7 +5,9 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { ActivatedRoute, ActivatedRouteSnapshot, Data, Params } from '@angular/router'; +import { ActivatedRoute, ActivatedRouteSnapshot, Data, Params, Router, RouterEvent, RouterStateSnapshot, RoutesRecognized } from '@angular/router'; + +import { Types } from './../../utils/types'; export function allData(value: ActivatedRouteSnapshot | ActivatedRoute): Data { let snapshot: ActivatedRouteSnapshot | null = value['snapshot'] || value; @@ -40,4 +42,22 @@ export function allParams(value: ActivatedRouteSnapshot | ActivatedRoute): Param } return result; +} + +export function childComponent(value: RouterStateSnapshot) { + let current = value.root; + + while (true) { + if (current.firstChild) { + current = current.firstChild; + } else { + break; + } + } + + return current.component; +} + +export function navigatedToOtherComponent(router: Router) { + return (e: RouterEvent) => Types.is(e, RoutesRecognized) && childComponent(e.state) !== childComponent(router.routerState.snapshot); } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/shortcut.component.ts b/src/Squidex/app/framework/angular/shortcut.component.ts index 01148f3fb..26191b2eb 100644 --- a/src/Squidex/app/framework/angular/shortcut.component.ts +++ b/src/Squidex/app/framework/angular/shortcut.component.ts @@ -5,13 +5,14 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from '@angular/core'; +import { ChangeDetectionStrategy, Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from '@angular/core'; import { ShortcutService } from './../services/shortcut.service'; @Component({ selector: 'sqx-shortcut', - template: '' + template: '', + changeDetection: ChangeDetectionStrategy.OnPush }) export class ShortcutComponent implements OnDestroy, OnInit { @Input() diff --git a/src/Squidex/app/framework/angular/title.component.ts b/src/Squidex/app/framework/angular/title.component.ts index 636e14a7b..20161b09a 100644 --- a/src/Squidex/app/framework/angular/title.component.ts +++ b/src/Squidex/app/framework/angular/title.component.ts @@ -5,13 +5,14 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, Input, OnChanges } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core'; import { TitleService } from './../services/title.service'; @Component({ selector: 'sqx-title', - template: '' + template: '', + changeDetection: ChangeDetectionStrategy.OnPush }) export class TitleComponent implements OnChanges { @Input() diff --git a/src/Squidex/app/framework/angular/user-report.component.ts b/src/Squidex/app/framework/angular/user-report.component.ts index 497a4b56f..0cb33edb7 100644 --- a/src/Squidex/app/framework/angular/user-report.component.ts +++ b/src/Squidex/app/framework/angular/user-report.component.ts @@ -5,14 +5,15 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { UserReportConfig } from './../configurations'; import { ResourceLoaderService } from './../services/resource-loader.service'; @Component({ selector: 'sqx-user-report', - template: '' + template: '', + changeDetection: ChangeDetectionStrategy.OnPush }) export class UserReportComponent implements OnDestroy, OnInit { private loadingTimer: any; diff --git a/src/Squidex/app/framework/declarations.ts b/src/Squidex/app/framework/declarations.ts index dda4dbf42..16cc7f7dd 100644 --- a/src/Squidex/app/framework/declarations.ts +++ b/src/Squidex/app/framework/declarations.ts @@ -27,6 +27,7 @@ export * from './angular/forms/toggle.component'; export * from './angular/forms/transform-input.directive'; export * from './angular/forms/validators'; +export * from './angular/http/loading.interceptor'; export * from './angular/http/http-extensions'; export * from './angular/modals/dialog-renderer.component'; diff --git a/src/Squidex/app/framework/internal.ts b/src/Squidex/app/framework/internal.ts index b3771aa42..299635a8b 100644 --- a/src/Squidex/app/framework/internal.ts +++ b/src/Squidex/app/framework/internal.ts @@ -11,6 +11,7 @@ export * from './angular/animations'; export * from './services/analytics.service'; export * from './services/clipboard.service'; export * from './services/dialog.service'; +export * from './services/loading.service'; export * from './services/local-store.service'; export * from './services/message-bus.service'; export * from './services/onboarding.service'; diff --git a/src/Squidex/app/framework/module.ts b/src/Squidex/app/framework/module.ts index 210f86a97..b5b802278 100644 --- a/src/Squidex/app/framework/module.ts +++ b/src/Squidex/app/framework/module.ts @@ -6,6 +6,7 @@ */ import { CommonModule } from '@angular/common'; +import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { ModuleWithProviders, NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; @@ -44,6 +45,8 @@ import { KeysPipe, KNumberPipe, LightenPipe, + LoadingInterceptor, + LoadingService, LocalStoreService, MessageBus, ModalDialogComponent, @@ -220,11 +223,17 @@ export class SqxFrameworkModule { ClipboardService, DialogService, LocalStoreService, + LoadingService, MessageBus, OnboardingService, ResourceLoaderService, ShortcutService, - TitleService + TitleService, + { + provide: HTTP_INTERCEPTORS, + useClass: LoadingInterceptor, + multi: true + } ] }; } diff --git a/src/Squidex/app/framework/services/loading.service.spec.ts b/src/Squidex/app/framework/services/loading.service.spec.ts new file mode 100644 index 000000000..2e7b8128d --- /dev/null +++ b/src/Squidex/app/framework/services/loading.service.spec.ts @@ -0,0 +1,114 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Event, NavigationError, NavigationStart } from '@angular/router'; +import { Subject } from 'rxjs'; + +import { LoadingService, LoadingServiceFactory } from './loading.service'; + +describe('LoadingService', () => { + let events = new Subject(); + + it('should instantiate from factory', () => { + const loadingService = LoadingServiceFactory({ events }); + + expect(loadingService).toBeDefined(); + }); + + it('should instantiate', () => { + const loadingService = new LoadingService({ events }); + + expect(loadingService).toBeDefined(); + + loadingService.ngOnDestroy(); + }); + + it('should set to loaded', () => { + const loadingService = new LoadingService({ events }); + + let state = false; + + loadingService.loading.subscribe(v => state = v); + loadingService.startLoading('1'); + + expect(state).toBeTruthy(); + }); + + it('should set to loaded on navigation start', () => { + const loadingService = new LoadingService({ events }); + + let state = false; + + loadingService.loading.subscribe(v => state = v); + + events.next(new NavigationStart(0, '')); + + expect(state).toBeTruthy(); + }); + + it('should not unset from loaded immediately', () => { + const loadingService = new LoadingService({ events }); + + let state = false; + + loadingService.loading.subscribe(v => state = v); + loadingService.startLoading('1'); + loadingService.completeLoading('1'); + + expect(state).toBeTruthy(); + }); + + it('should not unset from loaded delayed', (cb) => { + const loadingService = new LoadingService({ events }); + + let state = false; + + loadingService.loading.subscribe(v => state = v); + loadingService.startLoading('1'); + loadingService.completeLoading('1'); + + setTimeout(() => { + expect(state).toBeFalsy(); + + cb(); + }, 400); + }); + + it('should not unset from loaded delayed on navigation event', (cb) => { + const loadingService = new LoadingService({ events }); + + let state = false; + + loadingService.loading.subscribe(v => state = v); + events.next(new NavigationStart(0, '')); + events.next(new NavigationError(0, '', 0)); + + setTimeout(() => { + expect(state).toBeFalsy(); + + cb(); + }, 400); + }); + + it('should set back to loaded after several completions', (cb) => { + const loadingService = new LoadingService({ events }); + + let state = false; + + loadingService.loading.subscribe(v => state = v); + loadingService.startLoading('1'); + loadingService.completeLoading('1'); + loadingService.completeLoading('1'); + loadingService.startLoading('2'); + + setTimeout(() => { + expect(state).toBeTruthy(); + + cb(); + }, 400); + }); +}); diff --git a/src/Squidex/app/framework/services/loading.service.ts b/src/Squidex/app/framework/services/loading.service.ts new file mode 100644 index 000000000..78598a271 --- /dev/null +++ b/src/Squidex/app/framework/services/loading.service.ts @@ -0,0 +1,68 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Injectable, OnDestroy } from '@angular/core'; +import { NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Router } from '@angular/router'; +import { BehaviorSubject, Observable, Subscription } from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { Types } from './../utils/types'; + +export const LoadingServiceFactory = (router: Router) => { + return new LoadingService(router); +}; + +@Injectable() +export class LoadingService implements OnDestroy { + private readonly routerSubscription: Subscription; + private readonly loading$ = new BehaviorSubject(0); + private readonly loadingOperations: { [key: string]: boolean } = {}; + + public get loading(): Observable { + return this.loading$.pipe(map(x => x > 0)); + } + + constructor(router: Router) { + this.routerSubscription = + router.events.subscribe(event => { + if (Types.is(event, NavigationStart)) { + this.startLoading(event.id.toString()); + } else if ( + Types.is(event, NavigationEnd) || + Types.is(event, NavigationCancel) || + Types.is(event, NavigationError)) { + this.completeLoading(event.id.toString()); + } + }); + } + + public ngOnDestroy() { + this.routerSubscription.unsubscribe(); + } + + public startLoading(key: string) { + if (!this.loadingOperations[key]) { + this.loadingOperations[key] = true; + + this.loading$.next(this.loading$.value + 1); + } + } + + public completeLoading(key: string) { + if (this.loadingOperations[key]) { + delete this.loadingOperations[key]; + + setTimeout(() => { + const value = this.loading$.value; + + if (value > 0) { + this.loading$.next(value - 1); + } + }, 250); + } + } +} \ No newline at end of file diff --git a/src/Squidex/app/shared/components/app-form.component.ts b/src/Squidex/app/shared/components/app-form.component.ts index f997a3443..808139bf7 100644 --- a/src/Squidex/app/shared/components/app-form.component.ts +++ b/src/Squidex/app/shared/components/app-form.component.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; import { FormBuilder } from '@angular/forms'; import { @@ -18,7 +18,8 @@ import { @Component({ selector: 'sqx-app-form', styleUrls: ['./app-form.component.scss'], - templateUrl: './app-form.component.html' + templateUrl: './app-form.component.html', + changeDetection: ChangeDetectionStrategy.OnPush }) export class AppFormComponent { @Output() @@ -46,7 +47,7 @@ export class AppFormComponent { const request = new CreateAppDto(value.name, this.template); this.appsStore.create(request) - .subscribe(dto => { + .subscribe(() => { this.complete(); }, error => { this.createForm.submitFailed(error); diff --git a/src/Squidex/app/shared/components/asset.component.html b/src/Squidex/app/shared/components/asset.component.html index 9eeb0acd0..e27a1108d 100644 --- a/src/Squidex/app/shared/components/asset.component.html +++ b/src/Squidex/app/shared/components/asset.component.html @@ -7,7 +7,7 @@
- +
@@ -89,7 +89,7 @@
- +
@@ -130,9 +130,7 @@
-
-
-
+
diff --git a/src/Squidex/app/shared/components/asset.component.scss b/src/Squidex/app/shared/components/asset.component.scss index 27f07083a..0ccea99d0 100644 --- a/src/Squidex/app/shared/components/asset.component.scss +++ b/src/Squidex/app/shared/components/asset.component.scss @@ -254,6 +254,10 @@ $list-height: 2.375rem; min-width: 12rem; } } + + .upload-progress { + padding: .25rem 0; + } } .drop-overlay { @@ -279,24 +283,16 @@ $list-height: 2.375rem; } } -.progress { - &-background { - background: $color-border; - margin: (($list-height - .25rem) / 2) 0; - } - - &-bar { - background: $color-theme-blue; - } +.selectable { + cursor: pointer; +} - &-background, - &-bar { - height: .25rem; - } +.bg { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAIAAAC1nk4lAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAYdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjEuMWMqnEsAAACbSURBVGhD7c6hDQAxAMPA33+m7vYlJh7AoFKOBMbfyfyZRRsPgUUbD4FFGw+BRRsPgUUbD4FFGw+BRRsPgUUbD4FFGw+BRRsPgUUbD4FFGw+BRRsPgUUbD4E3o9kA7YFFGw+BRRsPgUUbD4FFGw+BRRsPgUUbD4FFGw+BRRsPgUUbD4FFGw+BRRsPgUUbD4FFGw+BRRsPgQejz7nPYYKl8IqSfgAAAABJRU5ErkJggg=='); } -.selectable { - cursor: pointer; +.bg2 { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAYdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjEuMWMqnEsAAAAsSURBVDhPY9iDF/zHC0Y1YwCoKhxgRGqG0jgA1AwcYFQzBoCqwgFGnuY9ewCdSg6FRg4gMAAAAABJRU5ErkJggg=='); } .tags { diff --git a/src/Squidex/app/shared/components/asset.component.ts b/src/Squidex/app/shared/components/asset.component.ts index 3de754ff8..02c40c200 100644 --- a/src/Squidex/app/shared/components/asset.component.ts +++ b/src/Squidex/app/shared/components/asset.component.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, EventEmitter, HostBinding, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, HostBinding, Input, OnDestroy, OnInit, Output } from '@angular/core'; import { FormBuilder, FormControl } from '@angular/forms'; import { Subscription } from 'rxjs'; import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; @@ -31,7 +31,8 @@ import { templateUrl: './asset.component.html', animations: [ fadeAnimation - ] + ], + changeDetection: ChangeDetectionStrategy.OnPush }) export class AssetComponent implements OnDestroy, OnInit { private tagSubscription: Subscription; @@ -91,6 +92,7 @@ export class AssetComponent implements OnDestroy, OnInit { private readonly appsState: AppsState, private readonly assetsService: AssetsService, private readonly authState: AuthService, + private readonly changeDetector: ChangeDetectorRef, private readonly dialogs: DialogService, private readonly formBuilder: FormBuilder ) { @@ -105,7 +107,7 @@ export class AssetComponent implements OnDestroy, OnInit { if (Types.is(dto, AssetDto)) { this.emitLoaded(dto); } else { - this.progress = dto; + this.setProgress(dto); } }, error => { this.dialogs.notifyError(error); @@ -145,7 +147,7 @@ export class AssetComponent implements OnDestroy, OnInit { }, error => { this.dialogs.notifyError(error); - this.setProgress(); + this.setProgress(0); }); } } @@ -159,8 +161,6 @@ export class AssetComponent implements OnDestroy, OnInit { this.assetsService.putAsset(this.appsState.appName, this.asset.id, requestDto, this.asset.version) .subscribe(dto => { this.updateAsset(this.asset.rename(requestDto.fileName, this.authState.user!.token, dto.version), true); - - this.renameCancel(); }, error => { this.dialogs.notifyError(error); @@ -194,10 +194,6 @@ export class AssetComponent implements OnDestroy, OnInit { this.renaming = false; } - private setProgress(progress = 0) { - this.progress = progress; - } - private emitFailed(error: any) { this.failed.emit(error); } @@ -210,6 +206,12 @@ export class AssetComponent implements OnDestroy, OnInit { this.updated.emit(asset); } + private setProgress(progress: number) { + this.progress = progress; + + this.changeDetector.detectChanges(); + } + private updateAsset(asset: AssetDto, emitEvent: boolean) { this.asset = asset; this.progress = 0; @@ -221,5 +223,7 @@ export class AssetComponent implements OnDestroy, OnInit { } this.renameCancel(); + + this.changeDetector.detectChanges(); } } \ No newline at end of file diff --git a/src/Squidex/app/shared/components/assets-list.component.ts b/src/Squidex/app/shared/components/assets-list.component.ts index 75d5601b3..51f3a1e16 100644 --- a/src/Squidex/app/shared/components/assets-list.component.ts +++ b/src/Squidex/app/shared/components/assets-list.component.ts @@ -7,7 +7,7 @@ // tslint:disable:prefer-for-of -import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; import { onErrorResumeNext } from 'rxjs/operators'; import { @@ -19,7 +19,8 @@ import { @Component({ selector: 'sqx-assets-list', styleUrls: ['./assets-list.component.scss'], - templateUrl: './assets-list.component.html' + templateUrl: './assets-list.component.html', + changeDetection: ChangeDetectionStrategy.OnPush }) export class AssetsListComponent { public newFiles = ImmutableArray.empty(); diff --git a/src/Squidex/app/shared/components/assets-selector.component.ts b/src/Squidex/app/shared/components/assets-selector.component.ts index 457929c2d..9dca033c3 100644 --- a/src/Squidex/app/shared/components/assets-selector.component.ts +++ b/src/Squidex/app/shared/components/assets-selector.component.ts @@ -7,7 +7,7 @@ // tslint:disable:prefer-for-of -import { Component, EventEmitter, OnInit, Output } from '@angular/core'; +import { ChangeDetectionStrategy, Component, EventEmitter, OnInit, Output } from '@angular/core'; import { onErrorResumeNext } from 'rxjs/operators'; import { @@ -23,7 +23,8 @@ import { templateUrl: './assets-selector.component.html', animations: [ fadeAnimation - ] + ], + changeDetection: ChangeDetectionStrategy.OnPush }) export class AssetsSelectorComponent implements OnInit { public selectedAssets: { [id: string]: AssetDto } = {}; diff --git a/src/Squidex/app/shared/components/geolocation-editor.component.ts b/src/Squidex/app/shared/components/geolocation-editor.component.ts index 7239840db..b47014d41 100644 --- a/src/Squidex/app/shared/components/geolocation-editor.component.ts +++ b/src/Squidex/app/shared/components/geolocation-editor.component.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { AfterViewInit, Component, ElementRef, forwardRef, ViewChild } from '@angular/core'; +import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, forwardRef, ViewChild } from '@angular/core'; import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms'; import { @@ -31,7 +31,8 @@ interface Geolocation { selector: 'sqx-geolocation-editor', styleUrls: ['./geolocation-editor.component.scss'], templateUrl: './geolocation-editor.component.html', - providers: [SQX_GEOLOCATION_EDITOR_CONTROL_VALUE_ACCESSOR] + providers: [SQX_GEOLOCATION_EDITOR_CONTROL_VALUE_ACCESSOR], + changeDetection: ChangeDetectionStrategy.OnPush }) export class GeolocationEditorComponent implements ControlValueAccessor, AfterViewInit { private callChange = (v: any) => { /* NOOP */ }; @@ -144,12 +145,12 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi } public updateValueByInput() { - let updateMap = this.geolocationForm.controls['latitude'].value !== null && - this.geolocationForm.controls['longitude'].value !== null; + const lat = this.geolocationForm.controls['latitude'].value; + const lng = this.geolocationForm.controls['longitude'].value; - this.value = this.geolocationForm.value; + this.updateValue(lat, lng); - if (updateMap) { + if (lat && lng) { this.updateMarker(true, true); } else { this.callChange(this.value); @@ -188,11 +189,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi if (!this.marker && !this.isDisabled) { const latlng = event.latlng.wrap(); - this.value = { - latitude: latlng.lat, - longitude: latlng.lng - }; - + this.updateValue(latlng.lat, latlng.lng); this.updateMarker(false, true); } }); @@ -225,11 +222,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi this.map.addListener('click', (event: any) => { if (!this.isDisabled) { - this.value = { - latitude: event.latLng.lat(), - longitude: event.latLng.lng() - }; - + this.updateValue(event.latLng.lat(), event.latLng.lng()); this.updateMarker(false, true); } }); @@ -252,8 +245,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi let lat = place.geometry.location.lat(); let lng = place.geometry.location.lng(); - this.value = { latitude: lat, longitude: lng }; - + this.updateValue(lat, lng); this.updateMarker(false, true); } } @@ -274,15 +266,30 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi this.updateMarker(true, true); } + private updateValue(lat: number, lng: number) { + this.value = { latitude: lat, longitude: lng }; + } + private updateMarker(zoom: boolean, fireEvent: boolean) { if (!this.isGoogleMaps) { - this.updateMarkerOSM(zoom, fireEvent); + this.updateMarkerOSM(zoom); } else { - this.updateMarkerGoogle(zoom, fireEvent); + this.updateMarkerGoogle(zoom); + } + + if (this.value) { + this.geolocationForm.setValue(this.value, { emitEvent: true, onlySelf: false }); + } else { + this.geolocationForm.reset(undefined, { emitEvent: true, onlySelf: false }); + } + + if (fireEvent) { + this.callChange(this.value); + this.callTouched(); } } - private updateMarkerOSM(zoom: boolean, fireEvent: boolean) { + private updateMarkerOSM(zoom: boolean) { if (this.value) { if (!this.marker) { this.marker = L.marker([0, 90], { draggable: true }).addTo(this.map); @@ -290,10 +297,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi this.marker.on('drag', (event: any) => { const latlng = event.latlng.wrap(); - this.value = { - latitude: latlng.lat, - longitude: latlng.lng - }; + this.updateValue(latlng.lat, latlng.lng); }); this.marker.on('dragend', () => { @@ -314,8 +318,6 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi } this.marker.setLatLng(latLng); - - this.geolocationForm.setValue(this.value, { emitEvent: false, onlySelf: false }); } else { if (this.marker) { this.marker.removeFrom(this.map); @@ -323,17 +325,10 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi } this.map.fitWorld(); - - this.geolocationForm.reset(undefined, { emitEvent: false, onlySelf: false }); - } - - if (fireEvent) { - this.callChange(this.value); - this.callTouched(); } } - private updateMarkerGoogle(zoom: boolean, fireEvent: boolean) { + private updateMarkerGoogle(zoom: boolean) { if (this.value) { if (!this.marker) { this.marker = new google.maps.Marker({ @@ -347,19 +342,12 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi this.marker.addListener('drag', (event: any) => { if (!this.isDisabled) { - this.value = { - latitude: event.latLng.lat(), - longitude: event.latLng.lng() - }; + this.updateValue(event.latLng.lat(), event.LatLng.lng()); } }); this.marker.addListener('dragend', (event: any) => { if (!this.isDisabled) { - this.value = { - latitude: event.latLng.lat(), - longitude: event.latLng.lng() - }; - + this.updateValue(event.latLng.lat(), event.LatLng.lng()); this.updateMarker(false, true); } }); @@ -375,8 +363,6 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi this.marker.setPosition(latLng); this.map.setZoom(12); - - this.geolocationForm.setValue(this.value, { emitEvent: false, onlySelf: false }); } else { if (this.marker) { this.marker.setMap(null); @@ -384,13 +370,6 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi } this.map.setCenter({ lat: 0, lng: 0 }); - - this.geolocationForm.reset(undefined, { emitEvent: false, onlySelf: false }); - } - - if (fireEvent) { - this.callChange(this.value); - this.callTouched(); } } } \ No newline at end of file diff --git a/src/Squidex/app/shared/components/help.component.ts b/src/Squidex/app/shared/components/help.component.ts index f7d7bf68f..19059f168 100644 --- a/src/Squidex/app/shared/components/help.component.ts +++ b/src/Squidex/app/shared/components/help.component.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { HelpService } from '@app/shared/internal'; @@ -13,11 +13,11 @@ import { HelpService } from '@app/shared/internal'; @Component({ selector: 'sqx-help', styleUrls: ['./help.component.scss'], - templateUrl: './help.component.html' + templateUrl: './help.component.html', + changeDetection: ChangeDetectionStrategy.OnPush }) export class HelpComponent { - public helpSections = - this.helpService.getHelp(this.route.snapshot.data['helpPage']); + public helpSections = this.helpService.getHelp(this.route.snapshot.data['helpPage']); constructor( private readonly helpService: HelpService, diff --git a/src/Squidex/app/shared/components/history-list.component.html b/src/Squidex/app/shared/components/history-list.component.html index 1581365f1..06753ae23 100644 --- a/src/Squidex/app/shared/components/history-list.component.html +++ b/src/Squidex/app/shared/components/history-list.component.html @@ -1,12 +1,12 @@ -
+
{{event.actor | sqxUserNameRef:null}} - +
{{event.created | sqxFromNow}}
diff --git a/src/Squidex/app/shared/components/history-list.component.ts b/src/Squidex/app/shared/components/history-list.component.ts index e5a7acb93..b749cf094 100644 --- a/src/Squidex/app/shared/components/history-list.component.ts +++ b/src/Squidex/app/shared/components/history-list.component.ts @@ -5,33 +5,20 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, Input } from '@angular/core'; -import { Observable } from 'rxjs'; +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; -import { - formatHistoryMessage, - HistoryEventDto, - UsersProviderService -} from '@app/shared/internal'; +import { HistoryEventDto } from '@app/shared/internal'; @Component({ selector: 'sqx-history-list', styleUrls: ['./history-list.component.scss'], - templateUrl: './history-list.component.html' + templateUrl: './history-list.component.html', + changeDetection: ChangeDetectionStrategy.OnPush }) export class HistoryListComponent { @Input() public events: HistoryEventDto; - constructor( - private readonly users: UsersProviderService - ) { - } - - public format(message: string): Observable { - return formatHistoryMessage(message, this.users); - } - public trackByEvent(index: number, event: HistoryEventDto) { return event.eventId; } diff --git a/src/Squidex/app/shared/components/history.component.ts b/src/Squidex/app/shared/components/history.component.ts index 727e53247..7182caec2 100644 --- a/src/Squidex/app/shared/components/history.component.ts +++ b/src/Squidex/app/shared/components/history.component.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { merge, Observable, timer } from 'rxjs'; import { delay, switchMap } from 'rxjs/operators'; @@ -22,7 +22,8 @@ import { @Component({ selector: 'sqx-history', styleUrls: ['./history.component.scss'], - templateUrl: './history.component.html' + templateUrl: './history.component.html', + changeDetection: ChangeDetectionStrategy.OnPush }) export class HistoryComponent { private readonly channel = this.calculateChannel(); @@ -32,7 +33,7 @@ export class HistoryComponent { timer(0, 10000), this.messageBus.of(HistoryChannelUpdated).pipe(delay(1000)) ).pipe( - switchMap(app => this.historyService.getHistory(this.appsState.appName, this.channel))); + switchMap(() => this.historyService.getHistory(this.appsState.appName, this.channel))); constructor( private readonly appsState: AppsState, diff --git a/src/Squidex/app/shared/components/language-selector.component.ts b/src/Squidex/app/shared/components/language-selector.component.ts index 3dc2783ff..b22da54f3 100644 --- a/src/Squidex/app/shared/components/language-selector.component.ts +++ b/src/Squidex/app/shared/components/language-selector.component.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core'; +import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core'; import { fadeAnimation, ModalModel } from '@app/shared/internal'; @@ -17,7 +17,8 @@ export interface Language { iso2Code: string; englishName: string; isMasterLangu templateUrl: './language-selector.component.html', animations: [ fadeAnimation - ] + ], + changeDetection: ChangeDetectionStrategy.OnPush }) export class LanguageSelectorComponent implements OnChanges, OnInit { public dropdown = new ModalModel(); diff --git a/src/Squidex/app/shared/components/markdown-editor.component.ts b/src/Squidex/app/shared/components/markdown-editor.component.ts index 8fbc06107..0b54f7711 100644 --- a/src/Squidex/app/shared/components/markdown-editor.component.ts +++ b/src/Squidex/app/shared/components/markdown-editor.component.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { AfterViewInit, Component, ElementRef, forwardRef, Renderer2, ViewChild } from '@angular/core'; +import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, forwardRef, Renderer2, ViewChild } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { @@ -25,7 +25,8 @@ export const SQX_MARKDOWN_EDITOR_CONTROL_VALUE_ACCESSOR: any = { selector: 'sqx-markdown-editor', styleUrls: ['./markdown-editor.component.scss'], templateUrl: './markdown-editor.component.html', - providers: [SQX_MARKDOWN_EDITOR_CONTROL_VALUE_ACCESSOR] + providers: [SQX_MARKDOWN_EDITOR_CONTROL_VALUE_ACCESSOR], + changeDetection: ChangeDetectionStrategy.OnPush }) export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewInit { private callChange = (v: any) => { /* NOOP */ }; diff --git a/src/Squidex/app/shared/components/pipes.ts b/src/Squidex/app/shared/components/pipes.ts index 860296f39..17e01c3ad 100644 --- a/src/Squidex/app/shared/components/pipes.ts +++ b/src/Squidex/app/shared/components/pipes.ts @@ -11,11 +11,57 @@ import { map } from 'rxjs/operators'; import { ApiUrlConfig, + formatHistoryMessage, + HistoryEventDto, MathHelper, UserDto, UsersProviderService } from '@app/shared/internal'; +@Pipe({ + name: 'sqxHistoryMessage', + pure: false +}) +export class HistoryMessagePipe implements OnDestroy, PipeTransform { + private subscription: Subscription; + private lastMessage: string; + private lastValue: string | null = null; + + constructor( + private readonly changeDetector: ChangeDetectorRef, + private readonly users: UsersProviderService + ) { + } + + public ngOnDestroy() { + if (this.subscription) { + this.subscription.unsubscribe(); + } + } + + public transform(event: HistoryEventDto): string | null { + if (!event) { + return this.lastValue; + } + + if (this.lastMessage !== event.message) { + this.lastMessage = event.message; + + if (this.subscription) { + this.subscription.unsubscribe(); + } + + this.subscription = formatHistoryMessage(event.message, this.users).subscribe(value => { + this.lastValue = value; + + this.changeDetector.markForCheck(); + }); + } + + return this.lastValue; + } +} + class UserAsyncPipe implements OnDestroy { private lastUserId: string; private lastValue: string | null = null; diff --git a/src/Squidex/app/shared/components/rich-editor.component.ts b/src/Squidex/app/shared/components/rich-editor.component.ts index 0375d4733..4789dd670 100644 --- a/src/Squidex/app/shared/components/rich-editor.component.ts +++ b/src/Squidex/app/shared/components/rich-editor.component.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { AfterViewInit, Component, ElementRef, EventEmitter, forwardRef, OnDestroy, Output, ViewChild } from '@angular/core'; +import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, EventEmitter, forwardRef, OnDestroy, Output, ViewChild } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { @@ -25,7 +25,8 @@ export const SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR: any = { selector: 'sqx-rich-editor', styleUrls: ['./rich-editor.component.scss'], templateUrl: './rich-editor.component.html', - providers: [SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR] + providers: [SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR], + changeDetection: ChangeDetectionStrategy.OnPush }) export class RichEditorComponent implements ControlValueAccessor, AfterViewInit, OnDestroy { private callChange = (v: any) => { /* NOOP */ }; @@ -105,7 +106,7 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit, self.tinyInitTimer = setTimeout(() => { self.tinyEditor.setContent(this.value || ''); - }, 500); + }, 1000); }, target: this.editor.nativeElement diff --git a/src/Squidex/app/shared/components/schema-category.component.html b/src/Squidex/app/shared/components/schema-category.component.html index 570ae4798..f86bc7c42 100644 --- a/src/Squidex/app/shared/components/schema-category.component.html +++ b/src/Squidex/app/shared/components/schema-category.component.html @@ -16,7 +16,7 @@