- {{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 @@
0">
{{selectionCount}} items selected:
-
+
+ 1">
+
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 @@
-
-
+
{{schema.displayName}}
diff --git a/src/Squidex/app/shared/components/schema-category.component.ts b/src/Squidex/app/shared/components/schema-category.component.ts
index 9d8ee6831..0feb35bc5 100644
--- a/src/Squidex/app/shared/components/schema-category.component.ts
+++ b/src/Squidex/app/shared/components/schema-category.component.ts
@@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
-import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
+import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
import { onErrorResumeNext } from 'rxjs/operators';
import {
@@ -24,7 +24,8 @@ import {
templateUrl: './schema-category.component.html',
animations: [
fadeAnimation
- ]
+ ],
+ changeDetection: ChangeDetectionStrategy.OnPush
})
export class SchemaCategoryComponent implements OnInit, OnChanges {
@Output()
@@ -36,6 +37,9 @@ export class SchemaCategoryComponent implements OnInit, OnChanges {
@Input()
public isReadonly: boolean;
+ @Input()
+ public routeSingletonToContent = false;
+
@Input()
public schemasFilter: string;
@@ -100,6 +104,10 @@ export class SchemaCategoryComponent implements OnInit, OnChanges {
this.schemasState.changeCategory(schema, this.name).pipe(onErrorResumeNext()).subscribe();
}
+ public schemaRoute(schema: SchemaDto) {
+ return schema.isSingleton && this.routeSingletonToContent ? [schema.name, schema.id] : [schema.name];
+ }
+
public trackBySchema(index: number, schema: SchemaDto) {
return schema.id;
}
diff --git a/src/Squidex/app/shared/guards/content-must-exist.guard.spec.ts b/src/Squidex/app/shared/guards/content-must-exist.guard.spec.ts
index 9bcadd2bf..96f0727b6 100644
--- a/src/Squidex/app/shared/guards/content-must-exist.guard.spec.ts
+++ b/src/Squidex/app/shared/guards/content-must-exist.guard.spec.ts
@@ -7,7 +7,7 @@
import { Router } from '@angular/router';
import { of } from 'rxjs';
-import { IMock, Mock, Times } from 'typemoq';
+import { IMock, It, Mock, Times } from 'typemoq';
import { ContentDto } from './../services/contents.service';
import { ContentsState } from './../state/contents.state';
@@ -42,7 +42,7 @@ describe('ContentMustExistGuard', () => {
expect(result!).toBeTruthy();
- contentsState.verify(x => x.select('123'), Times.once());
+ router.verify(x => x.navigate(It.isAny()), Times.never());
});
it('should load content and return false when not found', () => {
diff --git a/src/Squidex/app/shared/guards/content-must-exist.guard.ts b/src/Squidex/app/shared/guards/content-must-exist.guard.ts
index a96463a27..c295fdfa0 100644
--- a/src/Squidex/app/shared/guards/content-must-exist.guard.ts
+++ b/src/Squidex/app/shared/guards/content-must-exist.guard.ts
@@ -32,7 +32,7 @@ export class ContentMustExistGuard implements CanActivate {
this.router.navigate(['/404']);
}
}),
- map(u => u !== null));
+ map(u => !!u));
return result;
}
diff --git a/src/Squidex/app/shared/guards/must-be-authenticated.guard.spec.ts b/src/Squidex/app/shared/guards/must-be-authenticated.guard.spec.ts
index 7d49f0dc8..027ca52da 100644
--- a/src/Squidex/app/shared/guards/must-be-authenticated.guard.spec.ts
+++ b/src/Squidex/app/shared/guards/must-be-authenticated.guard.spec.ts
@@ -7,7 +7,7 @@
import { Router } from '@angular/router';
import { of } from 'rxjs';
-import { IMock, Mock, Times } from 'typemoq';
+import { IMock, It, Mock, Times } from 'typemoq';
import { AuthService } from '@app/shared';
@@ -52,5 +52,7 @@ describe('MustBeAuthenticatedGuard', () => {
});
expect(result!).toBeTruthy();
+
+ router.verify(x => x.navigate(It.isAny()), Times.never());
});
});
\ No newline at end of file
diff --git a/src/Squidex/app/shared/guards/must-be-not-authenticated.guard.spec.ts b/src/Squidex/app/shared/guards/must-be-not-authenticated.guard.spec.ts
index 303d67ef4..2bf342413 100644
--- a/src/Squidex/app/shared/guards/must-be-not-authenticated.guard.spec.ts
+++ b/src/Squidex/app/shared/guards/must-be-not-authenticated.guard.spec.ts
@@ -7,7 +7,7 @@
import { Router } from '@angular/router';
import { of } from 'rxjs';
-import { IMock, Mock, Times } from 'typemoq';
+import { IMock, It, Mock, Times } from 'typemoq';
import { AuthService } from '@app/shared';
@@ -52,5 +52,7 @@ describe('MustNotBeAuthenticatedGuard', () => {
});
expect(result!).toBeTruthy();
+
+ router.verify(x => x.navigate(It.isAny()), Times.never());
});
});
\ No newline at end of file
diff --git a/src/Squidex/app/shared/guards/schema-must-exist-published.guard.spec.ts b/src/Squidex/app/shared/guards/schema-must-exist-published.guard.spec.ts
index 9013eae45..560ff9a86 100644
--- a/src/Squidex/app/shared/guards/schema-must-exist-published.guard.spec.ts
+++ b/src/Squidex/app/shared/guards/schema-must-exist-published.guard.spec.ts
@@ -5,9 +5,9 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
-import { Router, RouterStateSnapshot } from '@angular/router';
+import { Router } from '@angular/router';
import { of } from 'rxjs';
-import { IMock, Mock, Times } from 'typemoq';
+import { IMock, It, Mock, Times } from 'typemoq';
import { SchemaDetailsDto } from './../services/schemas.service';
import { SchemasState } from './../state/schemas.state';
@@ -21,7 +21,6 @@ describe('SchemaMustExistPublishedGuard', () => {
};
let schemasState: IMock
;
- let state: RouterStateSnapshot = { url: 'current-url' };
let router: IMock;
let schemaGuard: SchemaMustExistPublishedGuard;
@@ -37,13 +36,13 @@ describe('SchemaMustExistPublishedGuard', () => {
let result: boolean;
- schemaGuard.canActivate(route, state).subscribe(x => {
+ schemaGuard.canActivate(route).subscribe(x => {
result = x;
}).unsubscribe();
expect(result!).toBeTruthy();
- schemasState.verify(x => x.select('123'), Times.once());
+ router.verify(x => x.navigate(It.isAny()), Times.never());
});
it('should load schema and return false when not found', () => {
@@ -52,7 +51,7 @@ describe('SchemaMustExistPublishedGuard', () => {
let result: boolean;
- schemaGuard.canActivate(route, state).subscribe(x => {
+ schemaGuard.canActivate(route).subscribe(x => {
result = x;
}).unsubscribe();
@@ -60,34 +59,4 @@ describe('SchemaMustExistPublishedGuard', () => {
router.verify(x => x.navigate(['/404']), Times.once());
});
-
- it('should load schema and return false when not found', () => {
- schemasState.setup(x => x.select('123'))
- .returns(() => of(null));
-
- let result: boolean;
-
- schemaGuard.canActivate(route, state).subscribe(x => {
- result = x;
- }).unsubscribe();
-
- expect(result!).toBeFalsy();
-
- router.verify(x => x.navigate(['/404']), Times.once());
- });
-
- it('should redirect to content when singleton', () => {
- schemasState.setup(x => x.select('123'))
- .returns(() => of({ isSingleton: true, id: 'schema-id' }));
-
- let result: boolean;
-
- schemaGuard.canActivate(route, state).subscribe(x => {
- result = x;
- }).unsubscribe();
-
- expect(result!).toBeFalsy();
-
- router.verify(x => x.navigate([state.url, 'schema-id']), Times.once());
- });
});
\ No newline at end of file
diff --git a/src/Squidex/app/shared/guards/schema-must-exist-published.guard.ts b/src/Squidex/app/shared/guards/schema-must-exist-published.guard.ts
index c74df86d9..81e3c48a6 100644
--- a/src/Squidex/app/shared/guards/schema-must-exist-published.guard.ts
+++ b/src/Squidex/app/shared/guards/schema-must-exist-published.guard.ts
@@ -6,7 +6,7 @@
*/
import { Injectable } from '@angular/core';
-import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
+import { ActivatedRouteSnapshot, CanActivate, Router } from '@angular/router';
import { Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators';
@@ -22,7 +22,7 @@ export class SchemaMustExistPublishedGuard implements CanActivate {
) {
}
- public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable {
+ public canActivate(route: ActivatedRouteSnapshot): Observable {
const schemaName = allParams(route)['schemaName'];
const result =
@@ -31,12 +31,8 @@ export class SchemaMustExistPublishedGuard implements CanActivate {
if (!dto || !dto.isPublished) {
this.router.navigate(['/404']);
}
-
- if (dto && dto.isSingleton && state.url.indexOf(dto.id) < 0) {
- this.router.navigate([state.url, dto.id]);
- }
}),
- map(s => s !== null && s.isPublished));
+ map(s => !!s && s.isPublished));
return result;
}
diff --git a/src/Squidex/app/shared/guards/schema-must-exist.guard.spec.ts b/src/Squidex/app/shared/guards/schema-must-exist.guard.spec.ts
index efa0a15dd..2c517be7b 100644
--- a/src/Squidex/app/shared/guards/schema-must-exist.guard.spec.ts
+++ b/src/Squidex/app/shared/guards/schema-must-exist.guard.spec.ts
@@ -41,8 +41,6 @@ describe('SchemaMustExistGuard', () => {
}).unsubscribe();
expect(result!).toBeTruthy();
-
- schemasState.verify(x => x.select('123'), Times.once());
});
it('should load schema and return false when not found', () => {
diff --git a/src/Squidex/app/shared/guards/schema-must-exist.guard.ts b/src/Squidex/app/shared/guards/schema-must-exist.guard.ts
index bcdf3f632..8e12f86db 100644
--- a/src/Squidex/app/shared/guards/schema-must-exist.guard.ts
+++ b/src/Squidex/app/shared/guards/schema-must-exist.guard.ts
@@ -32,7 +32,7 @@ export class SchemaMustExistGuard implements CanActivate {
this.router.navigate(['/404']);
}
}),
- map(s => s !== null));
+ map(s => !!s));
return result;
}
diff --git a/src/Squidex/app/shared/guards/schema-must-not-be-singleton.guard.spec.ts b/src/Squidex/app/shared/guards/schema-must-not-be-singleton.guard.spec.ts
new file mode 100644
index 000000000..ba845da60
--- /dev/null
+++ b/src/Squidex/app/shared/guards/schema-must-not-be-singleton.guard.spec.ts
@@ -0,0 +1,105 @@
+/*
+ * Squidex Headless CMS
+ *
+ * @license
+ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
+ */
+
+import { Router, RouterStateSnapshot, UrlSegment } from '@angular/router';
+import { of } from 'rxjs';
+import { IMock, It, Mock, Times } from 'typemoq';
+
+import { SchemaDetailsDto } from './../services/schemas.service';
+import { SchemasState } from './../state/schemas.state';
+import { SchemaMustNotBeSingletonGuard } from './schema-must-not-be-singleton.guard';
+
+describe('SchemaMustNotBeSingletonGuard', () => {
+ const route: any = {
+ params: {
+ schemaName: '123'
+ },
+ url: [
+ new UrlSegment('schemas', {}),
+ new UrlSegment('name', {}),
+ new UrlSegment('new', {})
+ ]
+ };
+
+ let schemasState: IMock;
+ let router: IMock;
+ let schemaGuard: SchemaMustNotBeSingletonGuard;
+
+ beforeEach(() => {
+ router = Mock.ofType();
+ schemasState = Mock.ofType();
+ schemaGuard = new SchemaMustNotBeSingletonGuard(schemasState.object, router.object);
+ });
+
+ it('should subscribe to schema and return true when not singleton', () => {
+ const state: RouterStateSnapshot = { url: 'schemas/name/' };
+
+ schemasState.setup(x => x.selectedSchema)
+ .returns(() => of({ id: '123', isSingleton: false }));
+
+ let result: boolean;
+
+ schemaGuard.canActivate(route, state).subscribe(x => {
+ result = x;
+ }).unsubscribe();
+
+ expect(result!).toBeTruthy();
+
+ router.verify(x => x.navigate(It.isAny()), Times.never());
+ });
+
+ it('should subscribe to schema and return false when not found', () => {
+ const state: RouterStateSnapshot = { url: 'schemas/name/' };
+
+ schemasState.setup(x => x.selectedSchema)
+ .returns(() => of(null));
+
+ let result: boolean;
+
+ schemaGuard.canActivate(route, state).subscribe(x => {
+ result = x;
+ }).unsubscribe();
+
+ expect(result!).toBeFalsy();
+
+ router.verify(x => x.navigate(It.isAny()), Times.never());
+ });
+
+ it('should redirect to content when singleton', () => {
+ const state: RouterStateSnapshot = { url: 'schemas/name/' };
+
+ schemasState.setup(x => x.selectedSchema)
+ .returns(() => of({ id: '123', isSingleton: true }));
+
+ let result: boolean;
+
+ schemaGuard.canActivate(route, state).subscribe(x => {
+ result = x;
+ }).unsubscribe();
+
+ expect(result!).toBeFalsy();
+
+ router.verify(x => x.navigate([state.url, '123']), Times.once());
+ });
+
+ it('should redirect to content when singleton on new page', () => {
+ const state: RouterStateSnapshot = { url: 'schemas/name/new/' };
+
+ schemasState.setup(x => x.selectedSchema)
+ .returns(() => of({ id: '123', isSingleton: true }));
+
+ let result: boolean;
+
+ schemaGuard.canActivate(route, state).subscribe(x => {
+ result = x;
+ }).unsubscribe();
+
+ expect(result!).toBeFalsy();
+
+ router.verify(x => x.navigate(['schemas/name/', '123']), Times.once());
+ });
+});
\ No newline at end of file
diff --git a/src/Squidex/app/shared/guards/schema-must-not-be-singleton.guard.ts b/src/Squidex/app/shared/guards/schema-must-not-be-singleton.guard.ts
new file mode 100644
index 000000000..73624cd09
--- /dev/null
+++ b/src/Squidex/app/shared/guards/schema-must-not-be-singleton.guard.ts
@@ -0,0 +1,42 @@
+/*
+ * Squidex Headless CMS
+ *
+ * @license
+ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
+ */
+
+import { Injectable } from '@angular/core';
+import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
+import { Observable } from 'rxjs';
+import { map, take, tap } from 'rxjs/operators';
+
+import { SchemasState } from './../state/schemas.state';
+
+@Injectable()
+export class SchemaMustNotBeSingletonGuard implements CanActivate {
+ constructor(
+ private readonly schemasState: SchemasState,
+ private readonly router: Router
+ ) {
+ }
+
+ public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable {
+ const result =
+ this.schemasState.selectedSchema.pipe(
+ take(1),
+ tap(dto => {
+ if (dto && dto.isSingleton) {
+ if (state.url.indexOf('/new') >= 0) {
+ const parentUrl = state.url.slice(0, state.url.indexOf(route.url[route.url.length - 1].path));
+
+ this.router.navigate([parentUrl, dto.id]);
+ } else {
+ this.router.navigate([state.url, dto.id]);
+ }
+ }
+ }),
+ map(s => !!s && !s.isSingleton));
+
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/src/Squidex/app/shared/internal.ts b/src/Squidex/app/shared/internal.ts
index 2a3e2ba5a..562a4393d 100644
--- a/src/Squidex/app/shared/internal.ts
+++ b/src/Squidex/app/shared/internal.ts
@@ -13,6 +13,7 @@ export * from './guards/must-be-authenticated.guard';
export * from './guards/must-be-not-authenticated.guard';
export * from './guards/schema-must-exist-published.guard';
export * from './guards/schema-must-exist.guard';
+export * from './guards/schema-must-not-be-singleton.guard';
export * from './guards/unset-app.guard';
export * from './guards/unset-content.guard';
diff --git a/src/Squidex/app/shared/module.ts b/src/Squidex/app/shared/module.ts
index 49e44f77b..3bcdc192d 100644
--- a/src/Squidex/app/shared/module.ts
+++ b/src/Squidex/app/shared/module.ts
@@ -45,6 +45,7 @@ import {
HelpService,
HistoryComponent,
HistoryListComponent,
+ HistoryMessagePipe,
HistoryService,
LanguageSelectorComponent,
LanguagesService,
@@ -64,6 +65,7 @@ import {
SchemaCategoryComponent,
SchemaMustExistGuard,
SchemaMustExistPublishedGuard,
+ SchemaMustNotBeSingletonGuard,
SchemasService,
SchemasState,
SearchFormComponent,
@@ -100,6 +102,7 @@ import {
HelpComponent,
HistoryComponent,
HistoryListComponent,
+ HistoryMessagePipe,
LanguageSelectorComponent,
MarkdownEditorComponent,
SchemaCategoryComponent,
@@ -124,6 +127,7 @@ import {
HelpComponent,
HistoryComponent,
HistoryListComponent,
+ HistoryMessagePipe,
LanguageSelectorComponent,
MarkdownEditorComponent,
RouterModule,
@@ -180,6 +184,7 @@ export class SqxSharedModule {
RulesState,
SchemaMustExistGuard,
SchemaMustExistPublishedGuard,
+ SchemaMustNotBeSingletonGuard,
SchemasService,
SchemasState,
UIService,
diff --git a/src/Squidex/app/shared/state/contents.forms.ts b/src/Squidex/app/shared/state/contents.forms.ts
index 348124935..0eba8de49 100644
--- a/src/Squidex/app/shared/state/contents.forms.ts
+++ b/src/Squidex/app/shared/state/contents.forms.ts
@@ -379,7 +379,7 @@ export class EditContentForm extends Form {
private addArrayItem(field: RootFieldDto, language: AppLanguageDto | null, partitionForm: FormArray) {
const itemForm = new FormGroup({});
- let isOptional = field.isLocalizable && language !== null && language.isOptional;
+ let isOptional = field.isLocalizable && !!language && language.isOptional;
for (let nested of field.nested) {
const nestedValidators = FieldValidatorsFactory.createValidators(nested, isOptional);
diff --git a/src/Squidex/app/shared/state/contents.state.ts b/src/Squidex/app/shared/state/contents.state.ts
index f220670bd..21961630b 100644
--- a/src/Squidex/app/shared/state/contents.state.ts
+++ b/src/Squidex/app/shared/state/contents.state.ts
@@ -273,7 +273,7 @@ export abstract class ContentsStateBase extends State {
}
public init(): Observable {
- this.next(s => ({ ...s, contentsPager: new Pager(0), contentsQuery: '', isArchive: false, isLoaded: false }));
+ this.next(s => ({ contents: ImmutableArray.of(), contentsPager: new Pager(0) }));
return this.loadInternal();
}
diff --git a/src/Squidex/app/shared/state/queries.spec.ts b/src/Squidex/app/shared/state/queries.spec.ts
index bac5abc65..8dc79c04f 100644
--- a/src/Squidex/app/shared/state/queries.spec.ts
+++ b/src/Squidex/app/shared/state/queries.spec.ts
@@ -68,12 +68,16 @@ describe('Queries', () => {
it('should forward add call to state', () => {
queries.add('key3', 'filter3');
+ expect(true).toBeTruthy();
+
uiState.verify(x => x.set('schemas.my-schema.queries.key3', 'filter3'), Times.once());
});
it('should forward remove call to state', () => {
queries.remove('key3');
+ expect(true).toBeTruthy();
+
uiState.verify(x => x.remove('schemas.my-schema.queries.key3'), Times.once());
});
});
\ No newline at end of file
diff --git a/src/Squidex/app/shell/pages/app/left-menu.component.ts b/src/Squidex/app/shell/pages/app/left-menu.component.ts
index 7ca74465c..b4f9a93ad 100644
--- a/src/Squidex/app/shell/pages/app/left-menu.component.ts
+++ b/src/Squidex/app/shell/pages/app/left-menu.component.ts
@@ -5,14 +5,15 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
-import { Component } from '@angular/core';
+import { ChangeDetectionStrategy, Component } from '@angular/core';
import { AppsState } from '@app/shared';
@Component({
selector: 'sqx-left-menu',
styleUrls: ['./left-menu.component.scss'],
- templateUrl: './left-menu.component.html'
+ templateUrl: './left-menu.component.html',
+ changeDetection: ChangeDetectionStrategy.OnPush
})
export class LeftMenuComponent {
constructor(public readonly appsState: AppsState
diff --git a/src/Squidex/app/shell/pages/internal/apps-menu.component.ts b/src/Squidex/app/shell/pages/internal/apps-menu.component.ts
index 44bd0ad0a..136000cf1 100644
--- a/src/Squidex/app/shell/pages/internal/apps-menu.component.ts
+++ b/src/Squidex/app/shell/pages/internal/apps-menu.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 {
AppsState,
@@ -20,7 +20,8 @@ import {
templateUrl: './apps-menu.component.html',
animations: [
fadeAnimation
- ]
+ ],
+ changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppsMenuComponent {
public addAppDialog = new DialogModel();
diff --git a/src/Squidex/app/shell/pages/internal/internal-area.component.html b/src/Squidex/app/shell/pages/internal/internal-area.component.html
index c6bbe47b6..ddd4dcbb5 100644
--- a/src/Squidex/app/shell/pages/internal/internal-area.component.html
+++ b/src/Squidex/app/shell/pages/internal/internal-area.component.html
@@ -1,6 +1,12 @@