Browse Source

Base components.

pull/345/head
Sebastian Stehle 7 years ago
parent
commit
a31ab53f21
  1. 21
      src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts
  2. 28
      src/Squidex/app/features/administration/pages/restore/restore-page.component.ts
  3. 18
      src/Squidex/app/features/administration/pages/users/user-page.component.ts
  4. 48
      src/Squidex/app/features/content/pages/content/content-page.component.ts
  5. 17
      src/Squidex/app/features/content/pages/contents/contents-filters-page.component.ts
  6. 29
      src/Squidex/app/features/content/pages/contents/contents-page.component.ts
  7. 2
      src/Squidex/app/features/content/shared/array-editor.component.html
  8. 21
      src/Squidex/app/features/content/shared/array-editor.component.ts
  9. 10
      src/Squidex/app/features/content/shared/assets-editor.component.html
  10. 36
      src/Squidex/app/features/content/shared/assets-editor.component.ts
  11. 10
      src/Squidex/app/features/content/shared/preview-button.component.html
  12. 39
      src/Squidex/app/features/content/shared/preview-button.component.ts
  13. 4
      src/Squidex/app/features/content/shared/references-editor.component.ts
  14. 27
      src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts
  15. 23
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts
  16. 29
      src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.ts
  17. 28
      src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.ts
  18. 16
      src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.ts
  19. 20
      src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts
  20. 24
      src/Squidex/app/features/settings/pages/backups/backups-page.component.ts
  21. 4
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts
  22. 21
      src/Squidex/app/features/settings/pages/languages/languages-page.component.ts
  23. 5
      src/Squidex/app/framework/angular/forms/autocomplete.component.ts
  24. 3
      src/Squidex/app/framework/angular/forms/code-editor.component.ts
  25. 4
      src/Squidex/app/framework/angular/forms/control-errors.component.ts
  26. 4
      src/Squidex/app/framework/angular/forms/date-time-editor.component.ts
  27. 2
      src/Squidex/app/framework/angular/forms/iframe-editor.component.ts
  28. 2
      src/Squidex/app/framework/angular/forms/json-editor.component.ts
  29. 1
      src/Squidex/app/framework/angular/forms/stars.component.ts
  30. 2
      src/Squidex/app/framework/angular/forms/tag-editor.component.html
  31. 3
      src/Squidex/app/framework/angular/forms/tag-editor.component.ts
  32. 4
      src/Squidex/app/framework/angular/forms/toggle.component.ts
  33. 25
      src/Squidex/app/framework/angular/ignore-scrollbar.directive.ts
  34. 19
      src/Squidex/app/framework/angular/image-source.directive.ts
  35. 13
      src/Squidex/app/framework/angular/modals/dialog-renderer.component.ts
  36. 4
      src/Squidex/app/framework/angular/modals/modal-dialog.component.html
  37. 27
      src/Squidex/app/framework/angular/modals/modal-dialog.component.ts
  38. 36
      src/Squidex/app/framework/angular/modals/modal-target.directive.ts
  39. 37
      src/Squidex/app/framework/angular/modals/modal-view.directive.ts
  40. 10
      src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.ts
  41. 4
      src/Squidex/app/framework/angular/modals/tooltip.component.ts
  42. 17
      src/Squidex/app/framework/angular/routers/parent-link.directive.ts
  43. 18
      src/Squidex/app/framework/angular/shortcut.component.spec.ts
  44. 2
      src/Squidex/app/framework/angular/sorted.directive.ts
  45. 51
      src/Squidex/app/framework/angular/stateful.component.ts
  46. 8
      src/Squidex/app/framework/angular/user-report.component.ts
  47. 2
      src/Squidex/app/framework/services/loading.service.ts
  48. 14
      src/Squidex/app/framework/state.ts
  49. 3
      src/Squidex/app/shared/components/app-form.component.ts
  50. 28
      src/Squidex/app/shared/components/asset.component.html
  51. 50
      src/Squidex/app/shared/components/asset.component.ts
  52. 18
      src/Squidex/app/shared/components/assets-selector.component.html
  53. 56
      src/Squidex/app/shared/components/assets-selector.component.ts
  54. 19
      src/Squidex/app/shared/components/comments.component.ts
  55. 1
      src/Squidex/app/shared/components/geolocation-editor.component.ts
  56. 4
      src/Squidex/app/shared/components/markdown-editor.component.ts
  57. 44
      src/Squidex/app/shared/components/permission.directive.ts
  58. 6
      src/Squidex/app/shared/components/rich-editor.component.ts
  59. 12
      src/Squidex/app/shared/components/schema-category.component.html
  60. 6
      src/Squidex/app/shared/components/schema-category.component.scss
  61. 65
      src/Squidex/app/shared/components/schema-category.component.ts
  62. 2
      src/Squidex/app/shared/state/apps.state.ts
  63. 17
      src/Squidex/app/shared/state/schemas.state.ts
  64. 19
      src/Squidex/app/shell/pages/internal/internal-area.component.ts
  65. 6
      src/Squidex/app/shell/pages/internal/profile-menu.component.html
  66. 47
      src/Squidex/app/shell/pages/internal/profile-menu.component.ts

21
src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts

@ -5,11 +5,11 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Subscription, timer } from 'rxjs';
import { Component, OnInit } from '@angular/core';
import { timer } from 'rxjs';
import { onErrorResumeNext, switchMap } from 'rxjs/operators';
import { DialogModel } from '@app/shared';
import { DialogModel, ResourceOwner } from '@app/shared';
import { EventConsumerDto } from './../../services/event-consumers.service';
import { EventConsumersState } from './../../state/event-consumers.state';
@ -19,27 +19,20 @@ import { EventConsumersState } from './../../state/event-consumers.state';
styleUrls: ['./event-consumers-page.component.scss'],
templateUrl: './event-consumers-page.component.html'
})
export class EventConsumersPageComponent implements OnDestroy, OnInit {
private timerSubscription: Subscription;
export class EventConsumersPageComponent extends ResourceOwner implements OnInit {
public eventConsumerErrorDialog = new DialogModel();
public eventConsumerError = '';
constructor(
public readonly eventConsumersState: EventConsumersState
) {
}
public ngOnDestroy() {
this.timerSubscription.unsubscribe();
super();
}
public ngOnInit() {
this.eventConsumersState.load().pipe(onErrorResumeNext()).subscribe();
this.timerSubscription =
timer(2000, 2000).pipe(switchMap(x => this.eventConsumersState.load(true, true).pipe(onErrorResumeNext())))
.subscribe();
this.takeOver(timer(2000, 2000).pipe(switchMap(() => this.eventConsumersState.load(true, true))));
}
public reload() {
@ -58,7 +51,7 @@ export class EventConsumersPageComponent implements OnDestroy, OnInit {
this.eventConsumersState.reset(es).pipe(onErrorResumeNext()).subscribe();
}
public trackByEventConsumer(index: number, es: EventConsumerDto) {
public trackByEventConsumer(es: EventConsumerDto) {
return es.name;
}

28
src/Squidex/app/features/administration/pages/restore/restore-page.component.ts

@ -5,15 +5,16 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { Subscription, timer } from 'rxjs';
import { filter, onErrorResumeNext, switchMap } from 'rxjs/operators';
import { timer } from 'rxjs';
import { onErrorResumeNext, switchMap } from 'rxjs/operators';
import {
AuthService,
BackupsService,
DialogService,
ResourceOwner,
RestoreDto,
RestoreForm
} from '@app/shared';
@ -23,9 +24,7 @@ import {
styleUrls: ['./restore-page.component.scss'],
templateUrl: './restore-page.component.html'
})
export class RestorePageComponent implements OnDestroy, OnInit {
private timerSubscription: Subscription;
export class RestorePageComponent extends ResourceOwner implements OnInit {
public restoreJob: RestoreDto | null;
public restoreForm = new RestoreForm(this.formBuilder);
@ -35,18 +34,17 @@ export class RestorePageComponent implements OnDestroy, OnInit {
private readonly dialogs: DialogService,
private readonly formBuilder: FormBuilder
) {
}
public ngOnDestroy() {
this.timerSubscription.unsubscribe();
super();
}
public ngOnInit() {
this.timerSubscription =
timer(0, 2000).pipe(switchMap(() => this.backupsService.getRestore().pipe(onErrorResumeNext())), filter(x => !!x))
.subscribe(dto => {
this.restoreJob = dto!;
});
this.takeOver(
timer(0, 2000).pipe(switchMap(() => this.backupsService.getRestore().pipe(onErrorResumeNext())))
.subscribe(job => {
if (job) {
this.restoreJob = job;
}
}));
}
public restore() {

18
src/Squidex/app/features/administration/pages/users/user-page.component.ts

@ -5,10 +5,11 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { ResourceOwner } from '@app/shared';
import { UserDto } from './../../services/users.service';
import { UserForm, UsersState } from './../../state/users.state';
@ -18,9 +19,7 @@ import { UserForm, UsersState } from './../../state/users.state';
styleUrls: ['./user-page.component.scss'],
templateUrl: './user-page.component.html'
})
export class UserPageComponent implements OnDestroy, OnInit {
private selectedUserSubscription: Subscription;
export class UserPageComponent extends ResourceOwner implements OnInit {
public canUpdate = false;
public user?: { user: UserDto, isCurrentUser: boolean };
@ -32,14 +31,11 @@ export class UserPageComponent implements OnDestroy, OnInit {
private readonly route: ActivatedRoute,
private readonly router: Router
) {
}
public ngOnDestroy() {
this.selectedUserSubscription.unsubscribe();
super();
}
public ngOnInit() {
this.selectedUserSubscription =
this.takeOver(
this.usersState.selectedUser
.subscribe(selectedUser => {
this.user = selectedUser!;
@ -47,7 +43,7 @@ export class UserPageComponent implements OnDestroy, OnInit {
if (selectedUser) {
this.userForm.load(selectedUser.user);
}
});
}));
}
public save() {

48
src/Squidex/app/features/content/pages/content/content-page.component.ts

@ -5,10 +5,10 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Component, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Observable, of, Subscription } from 'rxjs';
import { filter, onErrorResumeNext, switchMap } from 'rxjs/operators';
import { Observable, of } from 'rxjs';
import { onErrorResumeNext, switchMap } from 'rxjs/operators';
import { ContentVersionSelected } from './../messages';
@ -25,6 +25,7 @@ import {
LanguagesState,
MessageBus,
ModalModel,
ResourceOwner,
SchemaDetailsDto,
SchemasState,
Version
@ -40,12 +41,7 @@ import { DueTimeSelectorComponent } from './../../shared/due-time-selector.compo
fadeAnimation
]
})
export class ContentPageComponent implements CanComponentDeactivate, OnDestroy, OnInit {
private languagesSubscription: Subscription;
private contentSubscription: Subscription;
private contentVersionSelectedSubscription: Subscription;
private selectedSchemaSubscription: Subscription;
export class ContentPageComponent extends ResourceOwner implements CanComponentDeactivate, OnInit {
public schema: SchemaDetailsDto;
public content: ContentDto;
@ -70,44 +66,42 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy,
private readonly router: Router,
private readonly schemasState: SchemasState
) {
}
public ngOnDestroy() {
this.languagesSubscription.unsubscribe();
this.contentSubscription.unsubscribe();
this.contentVersionSelectedSubscription.unsubscribe();
this.selectedSchemaSubscription.unsubscribe();
super();
}
public ngOnInit() {
this.languagesSubscription =
this.takeOver(
this.languagesState.languages
.subscribe(languages => {
this.languages = languages.map(x => x.language);
this.language = this.languages.at(0);
});
}));
this.selectedSchemaSubscription =
this.schemasState.selectedSchema.pipe(filter(s => !!s))
this.takeOver(
this.schemasState.selectedSchema
.subscribe(schema => {
if (schema) {
this.schema = schema!;
this.contentForm = new EditContentForm(this.schema, this.languages);
});
}
}));
this.contentSubscription =
this.contentsState.selectedContent.pipe(filter(c => !!c))
this.takeOver(
this.contentsState.selectedContent
.subscribe(content => {
this.content = content!;
if (content) {
this.content = content;
this.loadContent(this.content.dataDraft);
});
}
}));
this.contentVersionSelectedSubscription =
this.takeOver(
this.messageBus.of(ContentVersionSelected)
.subscribe(message => {
this.loadVersion(message.version);
});
}));
}
public canDeactivate(): Observable<boolean> {

17
src/Squidex/app/features/content/pages/contents/contents-filters-page.component.ts

@ -5,13 +5,13 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Subscription } from 'rxjs';
import { Component, OnInit } from '@angular/core';
import { onErrorResumeNext } from 'rxjs/operators';
import {
ContentsState,
Queries,
ResourceOwner,
SchemasState,
UIState
} from '@app/shared';
@ -21,9 +21,7 @@ import {
styleUrls: ['./contents-filters-page.component.scss'],
templateUrl: './contents-filters-page.component.html'
})
export class ContentsFiltersPageComponent implements OnDestroy, OnInit {
private selectedSchemaSubscription: Subscription;
export class ContentsFiltersPageComponent extends ResourceOwner implements OnInit {
public schemaQueries: Queries;
constructor(
@ -31,20 +29,17 @@ export class ContentsFiltersPageComponent implements OnDestroy, OnInit {
private readonly schemasState: SchemasState,
private readonly uiState: UIState
) {
}
public ngOnDestroy() {
this.selectedSchemaSubscription.unsubscribe();
super();
}
public ngOnInit() {
this.selectedSchemaSubscription =
this.takeOver(
this.schemasState.selectedSchema
.subscribe(schema => {
if (schema) {
this.schemaQueries = new Queries(this.uiState, `schemas.${schema.name}`);
}
});
}));
}
public search(query: string) {

29
src/Squidex/app/features/content/pages/contents/contents-page.component.ts

@ -5,8 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Subscription } from 'rxjs';
import { Component, OnInit, ViewChild } from '@angular/core';
import { onErrorResumeNext, switchMap, tap } from 'rxjs/operators';
import {
@ -18,6 +17,7 @@ import {
LanguagesState,
ModalModel,
Queries,
ResourceOwner,
SchemaDetailsDto,
SchemasState,
UIState
@ -30,11 +30,7 @@ import { DueTimeSelectorComponent } from './../../shared/due-time-selector.compo
styleUrls: ['./contents-page.component.scss'],
templateUrl: './contents-page.component.html'
})
export class ContentsPageComponent implements OnDestroy, OnInit {
private contentsSubscription: Subscription;
private languagesSubscription: Subscription;
private selectedSchemaSubscription: Subscription;
export class ContentsPageComponent extends ResourceOwner implements OnInit {
public schema: SchemaDetailsDto;
public schemaQueries: Queries;
@ -61,16 +57,11 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
private readonly schemasState: SchemasState,
private readonly uiState: UIState
) {
}
public ngOnDestroy() {
this.contentsSubscription.unsubscribe();
this.languagesSubscription.unsubscribe();
this.selectedSchemaSubscription.unsubscribe();
super();
}
public ngOnInit() {
this.selectedSchemaSubscription =
this.takeOver(
this.schemasState.selectedSchema
.subscribe(schema => {
this.resetSelection();
@ -79,20 +70,20 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
this.schemaQueries = new Queries(this.uiState, `schemas.${this.schema.name}`);
this.contentsState.init().pipe(onErrorResumeNext()).subscribe();
});
}));
this.contentsSubscription =
this.takeOver(
this.contentsState.contents
.subscribe(() => {
this.updateSelectionSummary();
});
}));
this.languagesSubscription =
this.takeOver(
this.languagesState.languages
.subscribe(languages => {
this.languages = languages.map(x => x.language);
this.language = this.languages.at(0);
});
}));
}
public reload() {

2
src/Squidex/app/features/content/shared/array-editor.component.html

@ -5,7 +5,7 @@
<sqx-array-item
[form]="form"
[field]="field"
[isHidden]="isHidden"
[isHidden]="snapshot.isHidden"
[isFirst]="i === 0"
[isLast]="i === arrayControl.controls.length - 1"
[index]="i"

21
src/Squidex/app/features/content/shared/array-editor.component.ts

@ -5,23 +5,28 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input } from '@angular/core';
import { AbstractControl, FormArray, FormGroup } from '@angular/forms';
import {
AppLanguageDto,
EditContentForm,
ImmutableArray,
RootFieldDto
RootFieldDto,
StatefulComponent
} from '@app/shared';
interface State {
isHidden: boolean;
}
@Component({
selector: 'sqx-array-editor',
styleUrls: ['./array-editor.component.scss'],
templateUrl: './array-editor.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ArrayEditorComponent {
export class ArrayEditorComponent extends StatefulComponent<State> {
@Input()
public form: EditContentForm;
@ -37,10 +42,14 @@ export class ArrayEditorComponent {
@Input()
public arrayControl: FormArray;
public isHidden = false;
constructor(changeDetector: ChangeDetectorRef) {
super(changeDetector, {
isHidden: false
});
}
public hide(hide: boolean) {
this.isHidden = hide;
public hide(isHidden: boolean) {
this.next(s => ({ ...s, isHidden }));
}
public removeItem(index: number) {

10
src/Squidex/app/features/content/shared/assets-editor.component.html

@ -22,10 +22,10 @@
<div class="body">
<ng-container *ngIf="!snapshot.isListView; else listTemplate">
<div class="row no-gutters">
<sqx-asset *ngFor="let file of snapshot.newAssets" [initFile]="file"
<sqx-asset *ngFor="let file of snapshot.assetFiles" [initFile]="file"
(failed)="removeLoadingAsset(file)" (loaded)="addAsset(file, $event)">
</sqx-asset>
<sqx-asset *ngFor="let asset of snapshot.oldAssets; trackBy: trackByAsset" [asset]="asset" removeMode="true"
<sqx-asset *ngFor="let asset of snapshot.assets; trackBy: trackByAsset" [asset]="asset" removeMode="true"
(updated)="notifyOthers($event)" (removing)="removeLoadedAsset($event)">
</sqx-asset>
</div>
@ -33,14 +33,14 @@
<ng-template #listTemplate>
<div class="list-view">
<sqx-asset *ngFor="let file of snapshot.newAssets" [initFile]="file"
<sqx-asset *ngFor="let file of snapshot.assetFiles" [initFile]="file"
[isListView]="true" (failed)="removeLoadingAsset(file)" (loaded)="addAsset(file, $event)">
</sqx-asset>
<div
[sqxSortModel]="snapshot.oldAssets.values"
[sqxSortModel]="snapshot.assets.values"
(sqxSorted)="sortAssets($event)">
<div *ngFor="let asset of snapshot.oldAssets; trackBy: trackByAsset">
<div *ngFor="let asset of snapshot.assets; trackBy: trackByAsset">
<sqx-asset [asset]="asset" removeMode="true" [isListView]="true"
(updated)="notifyOthers($event)" (removing)="removeLoadedAsset($event)">
</sqx-asset>

36
src/Squidex/app/features/content/shared/assets-editor.component.ts

@ -35,9 +35,9 @@ class AssetUpdated {
}
interface State {
newAssets: ImmutableArray<File>;
assetFiles: ImmutableArray<File>;
oldAssets: ImmutableArray<AssetDto>;
assets: ImmutableArray<AssetDto>;
isListView: boolean;
}
@ -59,22 +59,22 @@ export class AssetsEditorComponent extends StatefulControlComponent<State, strin
private readonly messageBus: MessageBus
) {
super(changeDetector, {
oldAssets: ImmutableArray.empty(),
newAssets: ImmutableArray.empty(),
assets: ImmutableArray.empty(),
assetFiles: ImmutableArray.empty(),
isListView: localStore.getBoolean('squidex.assets.list-view')
});
}
public writeValue(obj: any) {
if (Types.isArrayOfString(obj)) {
if (!Types.isEquals(obj, this.snapshot.oldAssets.map(x => x.id).values)) {
if (!Types.isEquals(obj, this.snapshot.assets.map(x => x.id).values)) {
const assetIds: string[] = obj;
this.assetsService.getAssets(this.appsState.appName, 0, 0, undefined, undefined, obj)
.subscribe(dtos => {
this.setAssets(ImmutableArray.of(assetIds.map(id => dtos.items.find(x => x.id === id)!).filter(a => !!a)));
if (this.snapshot.oldAssets.length !== assetIds.length) {
if (this.snapshot.assets.length !== assetIds.length) {
this.updateValue();
}
}, () => {
@ -91,17 +91,17 @@ export class AssetsEditorComponent extends StatefulControlComponent<State, strin
}
public ngOnInit() {
this.observe(
this.takeOver(
this.messageBus.of(AssetUpdated)
.subscribe(event => {
if (event.source !== this) {
this.setAssets(this.snapshot.oldAssets.replaceBy('id', event.asset));
this.setAssets(this.snapshot.assets.replaceBy('id', event.asset));
}
}));
}
public setAssets(oldAssets: ImmutableArray<AssetDto>) {
this.next(s => ({ ...s, oldAssets }));
public setAssets(assets: ImmutableArray<AssetDto>) {
this.next(s => ({ ...s, assets }));
}
public pasteFiles(event: ClipboardEvent) {
@ -109,7 +109,7 @@ export class AssetsEditorComponent extends StatefulControlComponent<State, strin
const file = event.clipboardData.items[i].getAsFile();
if (file) {
this.next(s => ({ ...s, newAssets: s.newAssets.pushFront(file) }));
this.next(s => ({ ...s, assetFiles: s.assetFiles.pushFront(file) }));
}
}
}
@ -119,13 +119,13 @@ export class AssetsEditorComponent extends StatefulControlComponent<State, strin
const file = files[i];
if (file) {
this.next(s => ({ ...s, newAssets: s.newAssets.pushFront(file) }));
this.next(s => ({ ...s, assetFiles: s.assetFiles.pushFront(file) }));
}
}
}
public selectAssets(assets: AssetDto[]) {
this.setAssets(this.snapshot.oldAssets.push(...assets));
this.setAssets(this.snapshot.assets.push(...assets));
if (assets.length > 0) {
this.updateValue();
@ -138,8 +138,8 @@ export class AssetsEditorComponent extends StatefulControlComponent<State, strin
if (asset && file) {
this.next(s => ({
...s,
newAssets: s.newAssets.remove(file),
oldAssets: s.oldAssets.pushFront(asset)
assetFiles: s.assetFiles.remove(file),
assets: s.assets.pushFront(asset)
}));
this.updateValue();
@ -156,14 +156,14 @@ export class AssetsEditorComponent extends StatefulControlComponent<State, strin
public removeLoadedAsset(asset: AssetDto) {
if (asset) {
this.setAssets(this.snapshot.oldAssets.remove(asset));
this.setAssets(this.snapshot.assets.remove(asset));
this.updateValue();
}
}
public removeLoadingAsset(file: File) {
this.next(s => ({ ...s, newAssets: s.newAssets.remove(file) }));
this.next(s => ({ ...s, assetFiles: s.assetFiles.remove(file) }));
}
public changeView(isListView: boolean) {
@ -173,7 +173,7 @@ export class AssetsEditorComponent extends StatefulControlComponent<State, strin
}
private updateValue() {
let ids: string[] | null = this.snapshot.oldAssets.values.map(x => x.id);
let ids: string[] | null = this.snapshot.assets.values.map(x => x.id);
if (ids.length === 0) {
ids = null;

10
src/Squidex/app/features/content/shared/preview-button.component.html

@ -1,16 +1,16 @@
<ng-container *ngIf="selectedName">
<ng-container *ngIf="snapshot.selectedName">
<span>Preview: </span>
<div class="btn-group ml-1" #buttonGroup>
<button type="button" class="btn btn-secondary" (click)="follow(selectedName)">
<i class="icon-external-link"></i> {{selectedName}}
<button type="button" class="btn btn-secondary" (click)="follow(snapshot.selectedName)">
<i class="icon-external-link"></i> {{snapshot.selectedName}}
</button>
<div class="btn-group" *ngIf="alternativeNames.length > 0">
<div class="btn-group" *ngIf="snapshot.alternativeNames.length > 0">
<button type="button" class="btn btn-secondary dropdown-toggle" (click)="dropdown.toggle()"></button>
<div class="dropdown-menu" *sqxModalView="dropdown;closeAlways:true" [sqxModalTarget]="buttonGroup" @fade>
<a *ngFor="let name of alternativeNames" class="dropdown-item" (click)="follow(name)">{{name}}</a>
<a *ngFor="let name of snapshot.alternativeNames" class="dropdown-item" (click)="follow(name)">{{name}}</a>
</div>
</div>
</div>

39
src/Squidex/app/features/content/shared/preview-button.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
import {
ContentDto,
@ -13,9 +13,16 @@ import {
interpolate,
LocalStoreService,
ModalModel,
SchemaDetailsDto
SchemaDetailsDto,
StatefulComponent
} from '@app/shared';
interface State {
selectedName?: string;
alternativeNames: string[];
}
@Component({
selector: 'sqx-preview-button',
styleUrls: ['./preview-button.component.scss'],
@ -25,7 +32,7 @@ import {
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class PreviewButtonComponent implements OnInit {
export class PreviewButtonComponent extends StatefulComponent<State> implements OnInit {
@Input()
public content: ContentDto;
@ -34,13 +41,12 @@ export class PreviewButtonComponent implements OnInit {
public dropdown = new ModalModel();
public selectedName: string | undefined;
public alternativeNames: string[];
constructor(
constructor(changeDetector: ChangeDetectorRef,
private readonly localStore: LocalStoreService
) {
super(changeDetector, {
alternativeNames: []
});
}
public ngOnInit() {
@ -62,16 +68,23 @@ export class PreviewButtonComponent implements OnInit {
}
private selectUrl(selectedName: string) {
if (this.selectedName !== selectedName) {
this.next(s => {
if (selectedName === s.selectedName) {
return s;
}
const state = { ...s };
const keys = Object.keys(this.schema.previewUrls);
this.selectedName = selectedName;
state.selectedName = selectedName;
this.alternativeNames = keys.filter(x => x !== this.selectedName);
this.alternativeNames.sort();
state.alternativeNames = keys.filter(x => x !== s.selectedName);
state.alternativeNames.sort();
this.localStore.set(this.configKey(), selectedName);
}
return state;
});
}
private configKey() {

4
src/Squidex/app/features/content/shared/references-editor.component.ts

@ -29,7 +29,8 @@ export const SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
};
interface State {
schema?: SchemaDetailsDto;
schema?: SchemaDetailsDto | null;
schemaInvalid: boolean;
contentItems: ImmutableArray<ContentDto>;
@ -61,6 +62,7 @@ export class ReferencesEditorComponent extends StatefulControlComponent<State, s
) {
super(changeDetector, {
schemaInvalid: false,
schema: null,
contentItems: ImmutableArray.empty()
});
}

27
src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts

@ -5,8 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Subscription } from 'rxjs';
import { Component, OnInit } from '@angular/core';
import { filter, map, switchMap } from 'rxjs/operators';
import {
@ -17,6 +16,7 @@ import {
fadeAnimation,
HistoryEventDto,
HistoryService,
ResourceOwner,
UsagesService
} from '@app/shared';
@ -42,9 +42,7 @@ const COLORS = [
fadeAnimation
]
})
export class DashboardPageComponent implements OnDestroy, OnInit {
private subscriptions: Subscription[] = [];
export class DashboardPageComponent extends ResourceOwner implements OnInit {
public profileDisplayName = '';
public chartStorageCount: any;
@ -104,18 +102,11 @@ export class DashboardPageComponent implements OnDestroy, OnInit {
private readonly historyService: HistoryService,
private readonly usagesService: UsagesService
) {
}
public ngOnDestroy() {
for (let subscription of this.subscriptions) {
subscription.unsubscribe();
}
this.subscriptions = [];
super();
}
public ngOnInit() {
this.subscriptions.push(
this.takeOver(
this.app.pipe(
switchMap(app => this.usagesService.getTodayStorage(app.name)))
.subscribe(dto => {
@ -123,7 +114,7 @@ export class DashboardPageComponent implements OnDestroy, OnInit {
this.assetsMax = dto.maxAllowed;
}));
this.subscriptions.push(
this.takeOver(
this.app.pipe(
switchMap(app => this.usagesService.getMonthCalls(app.name)))
.subscribe(dto => {
@ -131,14 +122,14 @@ export class DashboardPageComponent implements OnDestroy, OnInit {
this.callsMax = dto.maxAllowed;
}));
this.subscriptions.push(
this.takeOver(
this.app.pipe(
switchMap(app => this.historyService.getHistory(app.name, '')))
.subscribe(dto => {
this.history = dto;
}));
this.subscriptions.push(
this.takeOver(
this.app.pipe(
switchMap(app => this.usagesService.getStorageUsages(app.name, DateTime.today().addDays(-20), DateTime.today())))
.subscribe(dtos => {
@ -175,7 +166,7 @@ export class DashboardPageComponent implements OnDestroy, OnInit {
};
}));
this.subscriptions.push(
this.takeOver(
this.app.pipe(
switchMap(app => this.usagesService.getCallsUsages(app.name, DateTime.today().addDays(-20), DateTime.today())))
.subscribe(dtos => {

23
src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts

@ -9,8 +9,7 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { filter, onErrorResumeNext } from 'rxjs/operators';
import { onErrorResumeNext } from 'rxjs/operators';
import {
AppsState,
@ -21,6 +20,7 @@ import {
MessageBus,
ModalModel,
PatternsState,
ResourceOwner,
SchemaDetailsDto,
SchemasState,
Types
@ -38,9 +38,7 @@ import {
fadeAnimation
]
})
export class SchemaPageComponent implements OnDestroy, OnInit {
private selectedSchemaSubscription: Subscription;
export class SchemaPageComponent extends ResourceOwner implements OnDestroy, OnInit {
public fieldTypes = fieldTypes;
public schemaExport: any;
@ -64,22 +62,21 @@ export class SchemaPageComponent implements OnDestroy, OnInit {
private readonly router: Router,
private readonly messageBus: MessageBus
) {
}
public ngOnDestroy() {
this.selectedSchemaSubscription.unsubscribe();
super();
}
public ngOnInit() {
this.patternsState.load().pipe(onErrorResumeNext()).subscribe();
this.selectedSchemaSubscription =
this.schemasState.selectedSchema.pipe(filter(s => !!s))
this.takeOver(
this.schemasState.selectedSchema
.subscribe(schema => {
this.schema = schema!;
if (schema) {
this.schema = schema;
this.export();
});
}
}));
}
public publish() {

29
src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.ts

@ -5,22 +5,24 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { Component, Input, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { Observable, Subscription } from 'rxjs';
import { Observable } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
import { FieldDto, FloatConverter, NumberFieldPropertiesDto } from '@app/shared';
import {
FieldDto,
FloatConverter,
NumberFieldPropertiesDto,
ResourceOwner
} from '@app/shared';
@Component({
selector: 'sqx-number-ui',
styleUrls: ['number-ui.component.scss'],
templateUrl: 'number-ui.component.html'
})
export class NumberUIComponent implements OnDestroy, OnInit {
private hideAllowedValuesSubscription: Subscription;
private hideInlineEditableSubscription: Subscription;
export class NumberUIComponent extends ResourceOwner implements OnInit {
@Input()
public editForm: FormGroup;
@ -35,11 +37,6 @@ export class NumberUIComponent implements OnDestroy, OnInit {
public hideAllowedValues: Observable<boolean>;
public hideInlineEditable: Observable<boolean>;
public ngOnDestroy() {
this.hideAllowedValuesSubscription.unsubscribe();
this.hideInlineEditableSubscription.unsubscribe();
}
public ngOnInit() {
this.editForm.setControl('editor',
new FormControl(this.properties.editor, [
@ -60,18 +57,18 @@ export class NumberUIComponent implements OnDestroy, OnInit {
this.editForm.controls['editor'].valueChanges.pipe(
startWith(this.properties.editor), map(x => !(x && (x === 'Input' || x === 'Dropdown'))));
this.hideAllowedValuesSubscription =
this.takeOver(
this.hideAllowedValues.subscribe(isSelection => {
if (isSelection) {
this.editForm.controls['allowedValues'].setValue(undefined);
}
});
}));
this.hideInlineEditableSubscription =
this.takeOver(
this.hideInlineEditable.subscribe(isSelection => {
if (isSelection) {
this.editForm.controls['inlineEditable'].setValue(false);
}
});
}));
}
}

28
src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.ts

@ -5,22 +5,23 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { Component, Input, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { Observable, Subscription } from 'rxjs';
import { Observable } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
import { FieldDto, StringFieldPropertiesDto } from '@app/shared';
import {
FieldDto,
ResourceOwner,
StringFieldPropertiesDto
} from '@app/shared';
@Component({
selector: 'sqx-string-ui',
styleUrls: ['string-ui.component.scss'],
templateUrl: 'string-ui.component.html'
})
export class StringUIComponent implements OnDestroy, OnInit {
private hideAllowedValuesSubscription: Subscription;
private hideInlineEditableSubscription: Subscription;
export class StringUIComponent extends ResourceOwner implements OnInit {
@Input()
public editForm: FormGroup;
@ -33,11 +34,6 @@ export class StringUIComponent implements OnDestroy, OnInit {
public hideAllowedValues: Observable<boolean>;
public hideInlineEditable: Observable<boolean>;
public ngOnDestroy() {
this.hideAllowedValuesSubscription.unsubscribe();
this.hideInlineEditableSubscription.unsubscribe();
}
public ngOnInit() {
this.editForm.setControl('editor',
new FormControl(this.properties.editor, [
@ -58,18 +54,18 @@ export class StringUIComponent implements OnDestroy, OnInit {
this.editForm.controls['editor'].valueChanges.pipe(
startWith(this.properties.editor), map(x => !(x && (x === 'Input' || x === 'Dropdown' || x === 'Slug'))));
this.hideAllowedValuesSubscription =
this.takeOver(
this.hideAllowedValues.subscribe(isSelection => {
if (isSelection) {
this.editForm.controls['allowedValues'].setValue(undefined);
}
});
}));
this.hideInlineEditableSubscription =
this.takeOver(
this.hideInlineEditable.subscribe(isSelection => {
if (isSelection) {
this.editForm.controls['inlineEditable'].setValue(false);
}
});
}));
}
}

16
src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.ts

@ -7,7 +7,7 @@
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { Observable, Subscription } from 'rxjs';
import { Observable } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
import {
@ -15,6 +15,7 @@ import {
FieldDto,
ImmutableArray,
ModalModel,
ResourceOwner,
RootFieldDto,
StringFieldPropertiesDto,
Types
@ -25,9 +26,7 @@ import {
styleUrls: ['string-validation.component.scss'],
templateUrl: 'string-validation.component.html'
})
export class StringValidationComponent implements OnDestroy, OnInit {
private patternSubscription: Subscription;
export class StringValidationComponent extends ResourceOwner implements OnDestroy, OnInit {
@Input()
public editForm: FormGroup;
@ -49,10 +48,6 @@ export class StringValidationComponent implements OnDestroy, OnInit {
public showUnique: boolean;
public ngOnDestroy() {
this.patternSubscription.unsubscribe();
}
public ngOnInit() {
this.showUnique = Types.is(this.field, RootFieldDto) && !this.field.isLocalizable;
@ -87,14 +82,15 @@ export class StringValidationComponent implements OnDestroy, OnInit {
this.showPatternMessage =
this.editForm.controls['pattern'].value && this.editForm.controls['pattern'].value.trim().length > 0;
this.patternSubscription =
this.takeOver(
this.editForm.controls['pattern'].valueChanges
.subscribe((value: string) => {
if (!value || value.length === 0) {
this.editForm.controls['patternMessage'].setValue(undefined);
}
this.setPatternName();
});
}));
this.setPatternName();
}

20
src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts

@ -5,10 +5,9 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormControl } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { map, onErrorResumeNext } from 'rxjs/operators';
import {
@ -16,6 +15,7 @@ import {
CreateCategoryForm,
DialogModel,
MessageBus,
ResourceOwner,
SchemaDto,
SchemasState
} from '@app/shared';
@ -27,9 +27,7 @@ import { SchemaCloning } from './../messages';
styleUrls: ['./schemas-page.component.scss'],
templateUrl: './schemas-page.component.html'
})
export class SchemasPageComponent implements OnDestroy, OnInit {
private schemaCloningSubscription: Subscription;
export class SchemasPageComponent extends ResourceOwner implements OnInit {
public addSchemaDialog = new DialogModel();
public addCategoryForm = new CreateCategoryForm(this.formBuilder);
@ -45,27 +43,25 @@ export class SchemasPageComponent implements OnDestroy, OnInit {
private readonly route: ActivatedRoute,
private readonly router: Router
) {
}
public ngOnDestroy() {
this.schemaCloningSubscription.unsubscribe();
super();
}
public ngOnInit() {
this.schemaCloningSubscription =
this.takeOver(
this.messageBus.of(SchemaCloning)
.subscribe(m => {
this.import = m.schema;
this.addSchemaDialog.show();
});
}));
this.takeOver(
this.route.params.pipe(map(q => q['showDialog']))
.subscribe(showDialog => {
if (showDialog) {
this.addSchemaDialog.show();
}
});
}));
this.schemasState.load().pipe(onErrorResumeNext()).subscribe();
}

24
src/Squidex/app/features/settings/pages/backups/backups-page.component.ts

@ -5,14 +5,15 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Subscription, timer } from 'rxjs';
import { Component, OnInit } from '@angular/core';
import { timer } from 'rxjs';
import { onErrorResumeNext, switchMap } from 'rxjs/operators';
import {
AppsState,
BackupDto,
BackupsState
BackupsState,
ResourceOwner
} from '@app/shared';
@Component({
@ -20,25 +21,20 @@ import {
styleUrls: ['./backups-page.component.scss'],
templateUrl: './backups-page.component.html'
})
export class BackupsPageComponent implements OnInit, OnDestroy {
private timerSubscription: Subscription;
export class BackupsPageComponent extends ResourceOwner implements OnInit {
constructor(
public readonly appsState: AppsState,
public readonly backupsState: BackupsState
) {
}
public ngOnDestroy() {
this.timerSubscription.unsubscribe();
super();
}
public ngOnInit() {
this.backupsState.load().pipe(onErrorResumeNext()).subscribe();
this.timerSubscription =
timer(3000, 3000).pipe(switchMap(t => this.backupsState.load(true, true).pipe(onErrorResumeNext())))
.subscribe();
this.takeOver(
timer(3000, 3000).pipe(switchMap(() => this.backupsState.load(true, true).pipe(onErrorResumeNext())))
.subscribe());
}
public reload() {
@ -53,7 +49,7 @@ export class BackupsPageComponent implements OnInit, OnDestroy {
this.backupsState.delete(backup).pipe(onErrorResumeNext()).subscribe();
}
public trackByBackup(index: number, item: BackupDto) {
public trackByBackup(item: BackupDto) {
return item.id;
}
}

4
src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts

@ -8,7 +8,7 @@
import { Component, Injectable, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { Observable } from 'rxjs';
import { filter, onErrorResumeNext, withLatestFrom } from 'rxjs/operators';
import { onErrorResumeNext, withLatestFrom } from 'rxjs/operators';
import {
AppContributorDto,
@ -34,7 +34,7 @@ export class UsersDataSource implements AutocompleteSource {
public find(query: string): Observable<any[]> {
return this.usersService.getUsers(query).pipe(
withLatestFrom(this.contributorsState.contributors.pipe(filter(x => !!x)), (users, contributors) => {
withLatestFrom(this.contributorsState.contributors, (users, contributors) => {
const results: any[] = [];
for (let user of users) {

21
src/Squidex/app/features/settings/pages/languages/languages-page.component.ts

@ -5,16 +5,16 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { Subscription } from 'rxjs';
import { onErrorResumeNext } from 'rxjs/operators';
import {
AddLanguageForm,
AppLanguageDto,
AppsState,
LanguagesState
LanguagesState,
ResourceOwner
} from '@app/shared';
@Component({
@ -22,9 +22,7 @@ import {
styleUrls: ['./languages-page.component.scss'],
templateUrl: './languages-page.component.html'
})
export class LanguagesPageComponent implements OnDestroy, OnInit {
private newLanguagesSubscription: Subscription;
export class LanguagesPageComponent extends ResourceOwner implements OnInit {
public addLanguageForm = new AddLanguageForm(this.formBuilder);
constructor(
@ -32,20 +30,17 @@ export class LanguagesPageComponent implements OnDestroy, OnInit {
public readonly languagesState: LanguagesState,
private readonly formBuilder: FormBuilder
) {
}
public ngOnDestroy() {
this.newLanguagesSubscription.unsubscribe();
super();
}
public ngOnInit() {
this.newLanguagesSubscription =
this.takeOver(
this.languagesState.newLanguages
.subscribe(languages => {
if (languages.length > 0) {
this.addLanguageForm.load({ language: languages.at(0) });
}
});
}));
this.languagesState.load().pipe(onErrorResumeNext()).subscribe();
}
@ -67,7 +62,7 @@ export class LanguagesPageComponent implements OnDestroy, OnInit {
}
}
public trackByLanguage(index: number, language: { language: AppLanguageDto }) {
public trackByLanguage(language: { language: AppLanguageDto }) {
return language.language;
}
}

5
src/Squidex/app/framework/angular/forms/autocomplete.component.ts

@ -10,7 +10,7 @@ import { FormControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Observable, of } from 'rxjs';
import { catchError, debounceTime, distinctUntilChanged, filter, map, switchMap, tap } from 'rxjs/operators';
import { StatefulControlComponent } from '@app/shared';
import { StatefulControlComponent } from '@app/framework/internal';
export interface AutocompleteSource {
find(query: string): Observable<any[]>;
@ -27,6 +27,7 @@ export const SQX_AUTOCOMPLETE_CONTROL_VALUE_ACCESSOR: any = {
interface State {
suggestedItems: any[];
suggestedIndex: number;
}
@ -66,7 +67,7 @@ export class AutocompleteComponent extends StatefulControlComponent<State, any[]
}
public ngOnInit() {
this.observe(
this.takeOver(
this.queryInput.valueChanges.pipe(
tap(query => {
this.callChange(query);

3
src/Squidex/app/framework/angular/forms/code-editor.component.ts

@ -41,8 +41,7 @@ export class CodeEditorComponent extends ExternalControlComponent<string> implem
@Input()
public mode = 'ace/mode/javascript';
constructor(
changeDetector: ChangeDetectorRef,
constructor(changeDetector: ChangeDetectorRef,
private readonly resourceLoader: ResourceLoaderService
) {
super(changeDetector);

4
src/Squidex/app/framework/angular/forms/control-errors.component.ts

@ -91,7 +91,7 @@ export class ControlErrorsComponent extends StatefulComponent<State> implements
this.control = control;
if (control) {
this.observe(
this.takeOver(
merge(control.valueChanges, control.statusChanges)
.subscribe(() => {
this.createMessages();
@ -127,6 +127,6 @@ export class ControlErrorsComponent extends StatefulComponent<State> implements
}
}
this.next(() => ({ errorMessages }));
this.next(s => ({ ...s, errorMessages }));
}
}

4
src/Squidex/app/framework/angular/forms/date-time-editor.component.ts

@ -60,7 +60,7 @@ export class DateTimeEditorComponent extends StatefulControlComponent<{}, string
}
public ngOnInit() {
this.observe(
this.takeOver(
this.timeControl.valueChanges.subscribe(value => {
if (!value || value.length === 0) {
this.timeValue = null;
@ -71,7 +71,7 @@ export class DateTimeEditorComponent extends StatefulControlComponent<{}, string
this.updateValue();
}));
this.observe(
this.takeOver(
this.dateControl.valueChanges.subscribe(value => {
if (!value || value.length === 0) {
this.dateValue = null;

2
src/Squidex/app/framework/angular/forms/iframe-editor.component.ts

@ -50,7 +50,7 @@ export class IFrameEditorComponent extends ExternalControlComponent<any> impleme
}
public ngOnInit(): void {
this.observe(
this.takeOver(
this.renderer.listen('window', 'message', (event: MessageEvent) => {
if (event.source === this.plugin.contentWindow) {
const { type } = event.data;

2
src/Squidex/app/framework/angular/forms/json-editor.component.ts

@ -39,6 +39,8 @@ export class JsonEditorComponent extends ExternalControlComponent<string> implem
private readonly resourceLoader: ResourceLoaderService
) {
super(changeDetector);
changeDetector.detach();
}
public writeValue(obj: any) {

1
src/Squidex/app/framework/angular/forms/stars.component.ts

@ -16,6 +16,7 @@ export const SQX_STARS_CONTROL_VALUE_ACCESSOR: any = {
interface State {
stars: number;
starsArray: number[];
value: number | null;

2
src/Squidex/app/framework/angular/forms/tag-editor.component.html

@ -2,7 +2,7 @@
<div class="form-control {{class}}" #form (click)="input.focus()"
[class.single-line]="singleLine"
[class.focus]="snapshot.hasFocus"
[class.disabled]snapshot.="addInput.disabled">
[class.disabled]="addInput.disabled">
<span class="item" *ngFor="let item of snapshot.items; let i = index" [class.disabled]="addInput.disabled">
{{item}} <i class="icon-close" (click)="remove(i)"></i>
</span>

3
src/Squidex/app/framework/angular/forms/tag-editor.component.ts

@ -78,6 +78,7 @@ interface State {
hasFocus: boolean;
suggestedItems: string[];
suggestedIndex: number;
items: any[];
@ -146,7 +147,7 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i
}
public ngOnInit() {
this.observe(
this.takeOver(
this.addInput.valueChanges.pipe(
tap(() => {
this.resetSize();

4
src/Squidex/app/framework/angular/forms/toggle.component.ts

@ -8,9 +8,7 @@
import { ChangeDetectorRef, Component, forwardRef, Input } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { Types } from '@app/framework/internal';
import { StatefulControlComponent } from '../stateful.component';
import { StatefulControlComponent, Types } from '@app/framework/internal';
export const SQX_TOGGLE_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ToggleComponent), multi: true

25
src/Squidex/app/framework/angular/ignore-scrollbar.directive.ts

@ -5,27 +5,23 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { AfterViewInit, Directive, ElementRef, OnDestroy, OnInit, Renderer2 } from '@angular/core';
import { AfterViewInit, Directive, ElementRef, OnInit, Renderer2 } from '@angular/core';
import { timer } from 'rxjs';
import { ResourceOwner } from '@app/framework/internal';
@Directive({
selector: '[sqxIgnoreScrollbar]'
})
export class IgnoreScrollbarDirective implements OnDestroy, OnInit, AfterViewInit {
private resizeListener: Function;
export class IgnoreScrollbarDirective extends ResourceOwner implements OnInit, AfterViewInit {
private parent: any;
private checkTimer: any;
private scollbarWidth = 0;
constructor(
private readonly element: ElementRef,
private readonly renderer: Renderer2
) {
}
public ngOnDestroy() {
clearTimeout(this.checkTimer);
this.resizeListener();
super();
}
public ngOnInit() {
@ -33,15 +29,12 @@ export class IgnoreScrollbarDirective implements OnDestroy, OnInit, AfterViewIni
this.parent = this.renderer.parentNode(this.element.nativeElement);
}
this.resizeListener =
this.takeOver(
this.renderer.listen(this.element.nativeElement, 'resize', () => {
this.reposition();
});
}));
this.checkTimer =
setTimeout(() => {
this.reposition();
}, 100);
this.takeOver(timer(100, 100).subscribe(() => this.reposition));
}
public ngAfterViewInit() {

19
src/Squidex/app/framework/angular/image-source.directive.ts

@ -7,18 +7,16 @@
import { AfterViewInit, Directive, ElementRef, HostListener, Input, OnChanges, OnDestroy, OnInit, Renderer2 } from '@angular/core';
import { MathHelper } from '@app/framework/internal';
import { MathHelper, ResourceOwner } from '@app/framework/internal';
const LAYOUT_CACHE: { [key: string]: { width: number, height: number } } = {};
@Directive({
selector: '[sqxImageSource]'
})
export class ImageSourceDirective implements OnChanges, OnDestroy, OnInit, AfterViewInit {
private parentResizeListener: Function;
private loadingTimer: any;
export class ImageSourceDirective extends ResourceOwner implements OnChanges, OnDestroy, OnInit, AfterViewInit {
private size: any;
private loadTimer: any;
private loadRetries = 0;
private loadQuery: string | null = null;
@ -38,12 +36,13 @@ export class ImageSourceDirective implements OnChanges, OnDestroy, OnInit, After
private readonly element: ElementRef,
private readonly renderer: Renderer2
) {
super();
}
public ngOnDestroy() {
clearTimeout(this.loadingTimer);
super.ngOnDestroy();
this.parentResizeListener();
clearTimeout(this.loadTimer);
}
public ngOnInit() {
@ -51,10 +50,10 @@ export class ImageSourceDirective implements OnChanges, OnDestroy, OnInit, After
this.parent = this.renderer.parentNode(this.element.nativeElement);
}
this.parentResizeListener =
this.takeOver(
this.renderer.listen(this.parent, 'resize', () => {
this.resize();
});
}));
}
public ngAfterViewInit() {
@ -127,7 +126,7 @@ export class ImageSourceDirective implements OnChanges, OnDestroy, OnInit, After
this.loadRetries++;
if (this.loadRetries <= 10) {
this.loadingTimer =
this.loadTimer =
setTimeout(() => {
this.loadQuery = MathHelper.guid();

13
src/Squidex/app/framework/angular/modals/dialog-renderer.component.ts

@ -13,11 +13,10 @@ import {
DialogRequest,
DialogService,
fadeAnimation,
Notification
Notification,
StatefulComponent
} from '@app/framework/internal';
import { StatefulComponent } from '../stateful.component';
interface State {
dialogRequest?: DialogRequest | null;
@ -46,14 +45,14 @@ export class DialogRendererComponent extends StatefulComponent<State> implements
}
public ngOnInit() {
this.observe(
this.takeOver(
this.dialogView.isOpen.subscribe(isOpen => {
if (!isOpen) {
this.finishRequest(false);
}
}));
this.observe(
this.takeOver(
this.dialogs.notifications.subscribe(notification => {
this.next(s => ({
...s,
@ -61,13 +60,13 @@ export class DialogRendererComponent extends StatefulComponent<State> implements
}));
if (notification.displayTime > 0) {
this.observe(timer(notification.displayTime).subscribe(() => {
this.takeOver(timer(notification.displayTime).subscribe(() => {
this.close(notification);
}));
}
}));
this.observe(
this.takeOver(
this.dialogs.dialogs
.subscribe(dialogRequest => {
this.cancel();

4
src/Squidex/app/framework/angular/modals/modal-dialog.component.html

@ -13,7 +13,7 @@
</button>
</div>
<div class="modal-tabs {{tabsClass}} clearfix" #tabsElement [hidden]="!showTabs || !hasTabs">
<div class="modal-tabs {{tabsClass}} clearfix" #tabsElement [hidden]="!showTabs || !snapshot.hasTabs">
<ng-content select="[tabs]"></ng-content>
</div>
@ -21,7 +21,7 @@
<ng-content select="[content]"></ng-content>
</div>
<div class="modal-footer" [hidden]="!showFooter || !hasFooter">
<div class="modal-footer" [hidden]="!showFooter || !snapshot.hasFooter">
<div class="clearfix" #footerElement>
<ng-content select="[footer]"></ng-content>
</div>

27
src/Squidex/app/framework/angular/modals/modal-dialog.component.ts

@ -7,7 +7,13 @@
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core';
import { fadeAnimation } from '@app/framework/internal';
import { fadeAnimation, StatefulComponent } from '@app/framework/internal';
interface State {
hasTabs: boolean;
hasFooter: boolean;
}
@Component({
selector: 'sqx-modal-dialog',
@ -18,7 +24,7 @@ import { fadeAnimation } from '@app/framework/internal';
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ModalDialogComponent implements AfterViewInit {
export class ModalDialogComponent extends StatefulComponent<State> implements AfterViewInit {
@Input()
public showClose = true;
@ -52,18 +58,17 @@ export class ModalDialogComponent implements AfterViewInit {
@ViewChild('footerElement')
public footerElement: ElementRef<ParentNode>;
public hasTabs = false;
public hasFooter = false;
constructor(
private readonly changeDetector: ChangeDetectorRef
) {
constructor(changeDetector: ChangeDetectorRef) {
super(changeDetector, {
hasTabs: false,
hasFooter: false
});
}
public ngAfterViewInit() {
this.hasTabs = this.tabsElement.nativeElement.children.length > 0;
this.hasFooter = this.footerElement.nativeElement.children.length > 0;
const hasTabs = this.tabsElement.nativeElement.children.length > 0;
const hasFooter = this.footerElement.nativeElement.children.length > 0;
this.changeDetector.detectChanges();
this.next(() => ({ hasTabs, hasFooter }));
}
}

36
src/Squidex/app/framework/angular/modals/modal-target.directive.ts

@ -5,7 +5,10 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { AfterViewInit, Directive, ElementRef, Input, OnDestroy, OnInit, Renderer2 } from '@angular/core';
import { AfterViewInit, Directive, ElementRef, Input, OnInit, Renderer2 } from '@angular/core';
import { timer } from 'rxjs';
import { ResourceOwner } from '@app/framework/internal';
const POSITION_TOPLEFT = 'topLeft';
const POSITION_TOPRIGHT = 'topRight';
@ -20,10 +23,7 @@ const POSITION_FULL = 'full';
@Directive({
selector: '[sqxModalTarget]'
})
export class ModalTargetDirective implements AfterViewInit, OnDestroy, OnInit {
private elementResizeListener: Function;
private targetResizeListener: Function;
private renderTimer: any;
export class ModalTargetDirective extends ResourceOwner implements AfterViewInit, OnInit {
private targetElement: any;
@Input('sqxModalTarget')
@ -42,38 +42,24 @@ export class ModalTargetDirective implements AfterViewInit, OnDestroy, OnInit {
private readonly renderer: Renderer2,
private readonly element: ElementRef
) {
}
public ngOnDestroy() {
if (this.targetResizeListener) {
this.targetResizeListener();
}
if (this.elementResizeListener) {
this.elementResizeListener();
}
clearInterval(this.renderTimer);
super();
}
public ngOnInit() {
if (this.target) {
this.targetElement = this.target;
this.targetResizeListener =
this.takeOver(
this.renderer.listen(this.targetElement, 'resize', () => {
this.updatePosition();
});
}));
this.elementResizeListener =
this.takeOver(
this.renderer.listen(this.element.nativeElement, 'resize', () => {
this.updatePosition();
});
}));
this.renderTimer =
setInterval(() => {
this.updatePosition();
}, 100);
this.takeOver(timer(100, 100).subscribe(() => this.updatePosition()));
}
}

37
src/Squidex/app/framework/angular/modals/modal-view.directive.ts

@ -6,22 +6,19 @@
*/
import { Directive, EmbeddedViewRef, Input, OnChanges, OnDestroy, Renderer2, SimpleChanges, TemplateRef, ViewContainerRef } from '@angular/core';
import { Subscription } from 'rxjs';
import {
DialogModel,
ModalModel,
ResourceOwner,
Types
} from '@app/framework/internal';
import { RootViewComponent } from './root-view.component';
@Directive({
selector: '[sqxModalView]'
})
export class ModalViewDirective implements OnChanges, OnDestroy {
private subscription: Subscription | null = null;
private documentClickListener: Function | null = null;
export class ModalViewDirective extends ResourceOwner implements OnChanges, OnDestroy {
private renderedView: EmbeddedViewRef<any> | null = null;
@Input('sqxModalView')
@ -42,11 +39,11 @@ export class ModalViewDirective implements OnChanges, OnDestroy {
private readonly viewContainer: ViewContainerRef,
private readonly rootView: RootViewComponent
) {
super();
}
public ngOnDestroy() {
this.unsubscribeToModal();
this.unsubscribeToClick();
super.ngOnDestroy();
if (Types.is(this.modalView, DialogModel) || Types.is(this.modalView, ModalModel)) {
this.modalView.hide();
@ -58,13 +55,13 @@ export class ModalViewDirective implements OnChanges, OnDestroy {
return;
}
this.unsubscribeToModal();
super.ngOnDestroy();
if (Types.is(this.modalView, DialogModel) || Types.is(this.modalView, ModalModel)) {
this.subscription =
this.takeOver(
this.modalView.isOpen.subscribe(isOpen => {
this.update(isOpen);
});
}));
} else {
this.update(!!this.modalView);
}
@ -95,7 +92,7 @@ export class ModalViewDirective implements OnChanges, OnDestroy {
this.renderedView = null;
this.unsubscribeToClick();
super.ngOnDestroy();
}
}
@ -108,7 +105,7 @@ export class ModalViewDirective implements OnChanges, OnDestroy {
return;
}
this.documentClickListener =
this.takeOver(
this.renderer.listen('document', 'click', (event: MouseEvent) => {
if (!event.target || this.renderedView === null) {
return;
@ -136,20 +133,6 @@ export class ModalViewDirective implements OnChanges, OnDestroy {
return;
}
}
});
}
private unsubscribeToModal() {
if (this.subscription) {
this.subscription.unsubscribe();
this.subscription = null;
}
}
private unsubscribeToClick() {
if (this.documentClickListener) {
this.documentClickListener();
this.documentClickListener = null;
}
}));
}
}

10
src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.ts

@ -26,8 +26,6 @@ import {
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnboardingTooltipComponent extends StatefulComponent implements OnDestroy, OnInit {
public tooltipModal = new ModalModel();
@Input()
public for: any;
@ -40,6 +38,8 @@ export class OnboardingTooltipComponent extends StatefulComponent implements OnD
@Input()
public position = 'left';
public tooltipModal = new ModalModel();
constructor(changeDetector: ChangeDetectorRef,
private readonly onboardingService: OnboardingService,
private readonly renderer: Renderer2
@ -55,7 +55,7 @@ export class OnboardingTooltipComponent extends StatefulComponent implements OnD
public ngOnInit() {
if (this.for && this.helpId && Types.isFunction(this.for.addEventListener)) {
this.observe(
this.takeOver(
timer(this.after).subscribe(() => {
if (this.onboardingService.shouldShow(this.helpId)) {
const forRect = this.for.getBoundingClientRect();
@ -68,7 +68,7 @@ export class OnboardingTooltipComponent extends StatefulComponent implements OnD
if (this.isSameOrParent(fromPoint)) {
this.tooltipModal.show();
this.observe(
this.takeOver(
timer(10000).subscribe(() => {
this.hideThis();
}));
@ -78,7 +78,7 @@ export class OnboardingTooltipComponent extends StatefulComponent implements OnD
}
}));
this.observe(
this.takeOver(
this.renderer.listen(this.for, 'mousedown', () => {
this.onboardingService.disable(this.helpId);

4
src/Squidex/app/framework/angular/modals/tooltip.component.ts

@ -39,12 +39,12 @@ export class TooltipComponent extends StatefulComponent implements OnDestroy, On
public ngOnInit() {
if (this.target) {
this.observe(
this.takeOver(
this.renderer.listen(this.target, 'mouseenter', () => {
this.modal.show();
}));
this.observe(
this.takeOver(
this.renderer.listen(this.target, 'mouseleave', () => {
this.modal.hide();
}));

17
src/Squidex/app/framework/angular/routers/parent-link.directive.ts

@ -7,13 +7,13 @@
import { Directive, ElementRef, HostListener, Input, OnDestroy, OnInit, Renderer2 } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { ResourceOwner } from '@app/framework/internal';
@Directive({
selector: '[sqxParentLink]'
})
export class ParentLinkDirective implements OnDestroy, OnInit {
private urlSubscription: Subscription;
export class ParentLinkDirective extends ResourceOwner implements OnDestroy, OnInit {
private url: string;
@Input()
@ -25,23 +25,18 @@ export class ParentLinkDirective implements OnDestroy, OnInit {
private readonly element: ElementRef,
private readonly renderer: Renderer2
) {
}
public ngOnDestroy() {
if (this.urlSubscription) {
this.urlSubscription.unsubscribe();
}
super();
}
public ngOnInit() {
this.urlSubscription =
this.takeOver(
this.route.url.subscribe(() => {
this.url = this.isLazyLoaded ?
this.router.createUrlTree(['.'], { relativeTo: this.route.parent!.parent }).toString() :
this.router.createUrlTree(['.'], { relativeTo: this.route.parent }).toString();
this.renderer.setProperty(this.element.nativeElement, 'href', this.url);
});
}));
}
@HostListener('click')

18
src/Squidex/app/framework/angular/shortcut.component.spec.ts

@ -11,6 +11,12 @@ import { ShortcutService } from './../';
import { ShortcutComponent } from './shortcut.component';
describe('ShortcutComponent', () => {
let changeDetector: any = {
detach: () => {
return 0;
}
};
let shortcutService: ShortcutService;
beforeEach(() => {
@ -18,13 +24,13 @@ describe('ShortcutComponent', () => {
});
it('should instantiate', () => {
const shortcutComponent = new ShortcutComponent(<any>{}, shortcutService, new NgZone({}));
const shortcutComponent = new ShortcutComponent(changeDetector, shortcutService, new NgZone({}));
expect(shortcutComponent).toBeDefined();
});
it('should init without keys', () => {
const shortcutComponent = new ShortcutComponent(<any>{}, shortcutService, new NgZone({}));
const shortcutComponent = new ShortcutComponent(changeDetector, shortcutService, new NgZone({}));
shortcutComponent.keys = null!;
shortcutComponent.ngOnInit();
@ -33,7 +39,7 @@ describe('ShortcutComponent', () => {
});
it('should destroy without keys', () => {
const shortcutComponent = new ShortcutComponent(<any>{}, shortcutService, new NgZone({}));
const shortcutComponent = new ShortcutComponent(changeDetector, shortcutService, new NgZone({}));
shortcutComponent.keys = null!;
shortcutComponent.ngOnDestroy();
@ -42,7 +48,7 @@ describe('ShortcutComponent', () => {
});
it('should raise event when triggered', () => {
const shortcutComponent = new ShortcutComponent(<any>{}, shortcutService, new NgZone({}));
const shortcutComponent = new ShortcutComponent(changeDetector, shortcutService, new NgZone({}));
let isTriggered = false;
@ -56,7 +62,7 @@ describe('ShortcutComponent', () => {
});
it('should not raise event when triggered but disabled', () => {
const shortcutComponent = new ShortcutComponent(<any>{}, shortcutService, new NgZone({}));
const shortcutComponent = new ShortcutComponent(changeDetector, shortcutService, new NgZone({}));
let isTriggered = false;
@ -71,7 +77,7 @@ describe('ShortcutComponent', () => {
});
it('should not raise event when triggered but destroyed', () => {
const shortcutComponent = new ShortcutComponent(<any>{}, shortcutService, new NgZone({}));
const shortcutComponent = new ShortcutComponent(changeDetector, shortcutService, new NgZone({}));
let isTriggered = false;

2
src/Squidex/app/framework/angular/sorted.directive.ts

@ -30,8 +30,10 @@ export class SortedDirective implements OnDestroy, OnInit {
}
public ngOnDestroy() {
if (this.sortable) {
this.sortable.destroy();
}
}
public ngOnInit() {
this.sortable = Sortable.create(this.elementRef.nativeElement, {

51
src/Squidex/app/framework/angular/stateful.component.ts

@ -7,7 +7,8 @@
import { ChangeDetectorRef, OnDestroy, OnInit } from '@angular/core';
import { ControlValueAccessor } from '@angular/forms';
import { Subscription } from 'rxjs';
import { Observable, Subscription } from 'rxjs';
import { onErrorResumeNext } from 'rxjs/operators';
import { Types } from './../utils/types';
@ -15,26 +16,19 @@ import { State } from '../state';
declare type UnsubscribeFunction = () => void;
export abstract class StatefulComponent<T = any> extends State<T> implements OnDestroy, OnInit {
export class ResourceOwner implements OnDestroy {
private subscriptions: (Subscription | UnsubscribeFunction)[] = [];
constructor(
private readonly changeDetector: ChangeDetectorRef,
state: T
) {
super(state);
}
protected observe(subscription: Subscription | UnsubscribeFunction) {
public takeOver<T>(subscription: Subscription | UnsubscribeFunction | Observable<T>) {
if (subscription) {
this.subscriptions.push(subscription);
if (Types.isFunction(subscription['subscribe'])) {
const observable = <Observable<T>>subscription;
this.subscriptions.push(observable.pipe(onErrorResumeNext()).subscribe());
} else {
this.subscriptions.push(<any>subscription);
}
}
public ngOnInit() {
this.changes.subscribe(() => {
this.changeDetector.detectChanges();
});
}
public ngOnDestroy() {
@ -52,6 +46,31 @@ export abstract class StatefulComponent<T = any> extends State<T> implements OnD
}
}
export abstract class StatefulComponent<T = any> extends State<T> implements OnDestroy, OnInit {
private readonly subscriptions = new ResourceOwner();
constructor(
private readonly changeDetector: ChangeDetectorRef,
state: T
) {
super(state);
}
public ngOnDestroy() {
this.subscriptions.ngOnDestroy();
}
public ngOnInit() {
this.changes.subscribe(() => {
this.changeDetector.detectChanges();
});
}
public takeOver<R>(subscription: Subscription | UnsubscribeFunction | Observable<R>) {
this.subscriptions.takeOver(subscription);
}
}
export abstract class StatefulControlComponent<T, TValue> extends StatefulComponent<T & { isDisabled: boolean }> implements ControlValueAccessor {
private fnChanged = (v: any) => { /* NOOP */ };
private fnTouched = () => { /* NOOP */ };

8
src/Squidex/app/framework/angular/user-report.component.ts

@ -10,7 +10,7 @@ import { timer } from 'rxjs';
import {
ResourceLoaderService,
StatefulComponent,
ResourceOwner,
UserReportConfig
} from '@app/framework/internal';
@ -18,12 +18,12 @@ import {
selector: 'sqx-user-report',
template: ''
})
export class UserReportComponent extends StatefulComponent<any> implements OnDestroy, OnInit {
export class UserReportComponent extends ResourceOwner implements OnDestroy, OnInit {
constructor(changeDetector: ChangeDetectorRef,
private readonly config: UserReportConfig,
private readonly resourceLoader: ResourceLoaderService
) {
super(changeDetector, {});
super();
changeDetector.detach();
}
@ -32,7 +32,7 @@ export class UserReportComponent extends StatefulComponent<any> implements OnDes
window['_urq'] = window['_urq'] || [];
window['_urq'].push(['initSite', this.config.siteId]);
this.observe(
this.takeOver(
timer(4000).subscribe(() => {
this.resourceLoader.loadScript('https://cdn.userreport.com/userreport.js');
}));

2
src/Squidex/app/framework/services/loading.service.ts

@ -41,8 +41,10 @@ export class LoadingService implements OnDestroy {
}
public ngOnDestroy() {
if (this.routerSubscription) {
this.routerSubscription.unsubscribe();
}
}
public startLoading(key: string) {
if (!this.loadingOperations[key]) {

14
src/Squidex/app/framework/state.ts

@ -9,7 +9,9 @@ import { AbstractControl } from '@angular/forms';
import { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { ErrorDto, Types } from '@app/framework/internal';
import { ErrorDto } from './utils/error';
import { Types } from './utils/types';
import { fullValue} from './angular/forms/forms-helper';
export interface FormState {
@ -133,10 +135,14 @@ export class State<T extends {}> {
}
public resetState() {
this.next(_ => this.initialState);
this.next(this.initialState);
}
public next(update: (v: T) => T) {
this.state.next(update(this.snapshot));
public next(update: ((v: T) => T) | object) {
if (Types.isFunction(update)) {
this.state.next(update(this.state.value));
} else {
this.state.next(Object.assign({}, this.snapshot, update));
}
}
}

3
src/Squidex/app/shared/components/app-form.component.ts

@ -30,7 +30,8 @@ export class AppFormComponent {
public createForm = new CreateAppForm(this.formBuilder);
constructor(public readonly apiUrl: ApiUrlConfig,
constructor(
public readonly apiUrl: ApiUrlConfig,
private readonly appsStore: AppsState,
private readonly formBuilder: FormBuilder
) {

28
src/Squidex/app/shared/components/asset.component.html

@ -1,7 +1,7 @@
<ng-container *ngIf="!isListView; else listTemplate">
<div class="card" [class.selectable]="isSelectable" [class.border-primary]="isSelected" (click)="selected.emit(asset)" (sqxFileDrop)="updateFile($event)">
<div class="card-body">
<div class="file-preview" *ngIf="asset && progress === 0" @fade>
<div class="file-preview" *ngIf="asset && snapshot.progress === 0" @fade>
<span class="file-type" *ngIf="asset.fileType">
{{asset.fileType}}
</span>
@ -44,11 +44,11 @@
</div>
</div>
<div class="upload-progress" *ngIf="progress > 0">
<sqx-progress-bar mode="Circle" [value]="progress"></sqx-progress-bar>
<div class="upload-progress" *ngIf="snapshot.progress > 0">
<sqx-progress-bar mode="Circle" [value]="snapshot.progress"></sqx-progress-bar>
</div>
<div class="drop-overlay align-items-center justify-content-center" *ngIf="asset && progress === 0">
<div class="drop-overlay align-items-center justify-content-center" *ngIf="asset && snapshot.progress === 0">
<div class="drop-overlay-background"></div>
<div class="drop-overlay-text">Drop to update</div>
</div>
@ -56,10 +56,10 @@
<div class="card-footer">
<ng-container *ngIf="asset">
<div>
<div *ngIf="!renaming" class="file-name editable" (dblclick)="renameStart()">
<div *ngIf="!snapshot.isRenaming" class="file-name editable" (dblclick)="renameStart()">
{{asset.fileName}}
</div>
<div *ngIf="renaming">
<div *ngIf="snapshot.isRenaming">
<form [formGroup]="renameForm.form" (ngSubmit)="renameAsset()">
<sqx-control-errors for="name" [submitted]="renameForm.submitted | async"></sqx-control-errors>
@ -88,19 +88,19 @@
<div class="table-items-row" [class.selectable]="isSelectable" (click)="selected.emit(asset)" (sqxFileDrop)="updateFile($event)">
<div class="left-border" [class.hidden]="!isSelectable" [class.selected]="isSelected" ></div>
<div *ngIf="asset && asset.canPreview && progress === 0" class="image drag-handle" [class.image-left]="!isSelectable" @fade>
<div *ngIf="asset && asset.canPreview && snapshot.progress === 0" class="image drag-handle" [class.image-left]="!isSelectable" @fade>
<img [sqxImageSource]="asset | sqxAssetPreviewUrl" class="bg2" layoutKey="asset-small">
</div>
<div *ngIf="asset && !asset.canPreview && progress === 0" class="image drag-handle image-padded" [class.image-left]="!isSelectable" @fade>
<div *ngIf="asset && !asset.canPreview && snapshot.progress === 0" class="image drag-handle image-padded" [class.image-left]="!isSelectable" @fade>
<img class="icon" [attr.src]="asset | sqxFileIcon">
</div>
<div class="row no-gutters" *ngIf="asset && progress === 0" @fade>
<div class="row no-gutters" *ngIf="asset && snapshot.progress === 0" @fade>
<div class="col col-name">
<div *ngIf="!renaming" class="file-name editable" (dblclick)="renameStart()">
<div *ngIf="!snapshot.isRenaming" class="file-name editable" (dblclick)="renameStart()">
{{asset.fileName}}
</div>
<div *ngIf="renaming">
<div *ngIf="snapshot.isRenaming">
<form [formGroup]="renameForm.form" (ngSubmit)="renameAsset()">
<sqx-control-errors for="name" [submitted]="renameForm.submitted | async"></sqx-control-errors>
@ -129,11 +129,11 @@
</div>
</div>
<div class="upload-progress" *ngIf="progress > 0">
<sqx-progress-bar [value]="progress" [trailWidth]="0.8" [strokeWidth]="0.8" [showText]="false"></sqx-progress-bar>
<div class="upload-progress" *ngIf="snapshot.progress > 0">
<sqx-progress-bar [value]="snapshot.progress" [trailWidth]="0.8" [strokeWidth]="0.8" [showText]="false"></sqx-progress-bar>
</div>
<div class="drop-overlay align-items-center justify-content-center" *ngIf="asset && progress === 0">
<div class="drop-overlay align-items-center justify-content-center" *ngIf="asset && snapshot.progress === 0">
<div class="drop-overlay-background"></div>
<div class="drop-overlay-text">Drop to update</div>
</div>

50
src/Squidex/app/shared/components/asset.component.ts

@ -5,9 +5,8 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, HostBinding, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, HostBinding, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
import { FormBuilder, FormControl } from '@angular/forms';
import { Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
import {
@ -20,11 +19,20 @@ import {
fadeAnimation,
RenameAssetDto,
RenameAssetForm,
StatefulComponent,
TagAssetDto,
Types,
Versioned
} from '@app/shared/internal';
interface State {
isTagging: boolean;
isRenaming: boolean;
progress: number;
}
@Component({
selector: 'sqx-asset',
styleUrls: ['./asset.component.scss'],
@ -34,9 +42,7 @@ import {
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AssetComponent implements OnChanges, OnDestroy, OnInit {
private tagSubscription: Subscription;
export class AssetComponent extends StatefulComponent<State> implements OnChanges, OnInit {
@Input()
public initFile: File;
@ -79,27 +85,22 @@ export class AssetComponent implements OnChanges, OnDestroy, OnInit {
@Output()
public failed = new EventEmitter();
public isTagging = false;
public renaming = false;
public renameForm = new RenameAssetForm(this.formBuilder);
public tagInput = new FormControl();
public progress = 0;
constructor(
constructor(changeDetector: ChangeDetectorRef,
private readonly appsState: AppsState,
private readonly assetsService: AssetsService,
private readonly authState: AuthService,
private readonly changeDetector: ChangeDetectorRef,
private readonly dialogs: DialogService,
private readonly formBuilder: FormBuilder
) {
}
public ngOnDestroy() {
this.tagSubscription.unsubscribe();
super(changeDetector, {
isRenaming: false,
isTagging: false,
progress: 0
});
}
public ngOnInit() {
@ -122,13 +123,13 @@ export class AssetComponent implements OnChanges, OnDestroy, OnInit {
});
}
this.tagSubscription =
this.takeOver(
this.tagInput.valueChanges.pipe(
distinctUntilChanged(),
debounceTime(2000)
).subscribe(tags => {
this.tagAsset(tags);
});
}));
}
public ngOnChanges(changes: SimpleChanges) {
@ -189,13 +190,15 @@ export class AssetComponent implements OnChanges, OnDestroy, OnInit {
public renameStart() {
if (!this.isDisabled) {
this.renameForm.load(this.asset);
this.renaming = true;
this.next(s => ({ ...s, isRenaming: true }));
}
}
public renameCancel() {
this.renameForm.submitCompleted();
this.renaming = false;
this.next(s => ({ ...s, isRenaming: false }));
}
private emitFailed(error: any) {
@ -211,14 +214,11 @@ export class AssetComponent implements OnChanges, OnDestroy, OnInit {
}
private setProgress(progress: number) {
this.progress = progress;
this.changeDetector.markForCheck();
this.next(s => ({ ...s, progress }));
}
private updateAsset(asset: AssetDto, emitEvent: boolean) {
this.asset = asset;
this.progress = 0;
this.tagInput.setValue(asset.tags, { emitEvent: false });
@ -228,6 +228,6 @@ export class AssetComponent implements OnChanges, OnDestroy, OnInit {
this.renameCancel();
this.changeDetector.markForCheck();
this.next(s => ({ ...s, progress: 0 }));
}
}

18
src/Squidex/app/shared/components/assets-selector.component.html

@ -14,8 +14,8 @@
<div class="row no-gutters search">
<div class="col-6">
<sqx-tag-editor class="tags" singleLine="true" placeholder="Search by tags"
[suggestions]="state.tagsNames | async"
[ngModel]="state.selectedTagNames | async"
[suggestions]="assetsState.tagsNames | async"
[ngModel]="assetsState.selectedTagNames | async"
(ngModelChange)="selectTags($event)"
[undefinedWhenEmpty]="false">
</sqx-tag-editor>
@ -23,7 +23,7 @@
<div class="col-6">
<sqx-search-form formClass="form" placeholder="Search by asset name" fieldExample="fileSize"
(queryChanged)="search($event)"
[query]="state.assetsQuery | async"
[query]="assetsState.assetsQuery | async"
enableShortcut="true">
</sqx-search-form>
</div>
@ -31,10 +31,10 @@
</div>
<div class="col-auto pl-1">
<div class="btn-group" data-toggle="buttons">
<button type="button" class="btn btn-secondary btn-toggle" [class.btn-primary]="isListView" [disabled]="isListView" (click)="changeView(true)">
<button type="button" class="btn btn-secondary btn-toggle" [class.btn-primary]="snapshot.isListView" [disabled]="snapshot.isListView" (click)="changeView(true)">
<i class="icon-list"></i>
</button>
<button type="button" class="btn btn-secondary btn-toggle" [class.btn-primary]="!isListView" [disabled]="!isListView" (click)="changeView(false)">
<button type="button" class="btn btn-secondary btn-toggle" [class.btn-primary]="!snapshot.isListView" [disabled]="!snapshot.isListView" (click)="changeView(false)">
<i class="icon-grid"></i>
</button>
</div>
@ -44,15 +44,15 @@
<ng-container content>
<sqx-assets-list
[isListView]="isListView"
[isListView]="snapshot.isListView"
(selected)="selectAsset($event)"
[selectedIds]="selectedAssets"
[state]="state" isDisabled="true">
[selectedIds]="snapshot.selectedAssets"
[state]="assetsState" isDisabled="true">
</sqx-assets-list>
</ng-container>
<ng-container footer>
<button type="reset" class="float-left btn btn-secondary" (click)="complete()">Cancel</button>
<button type="submit" class="float-right btn btn-success" (click)="select()" [disabled]="selectionCount === 0">Link selected assets ({{selectionCount}})</button>
<button type="submit" class="float-right btn btn-success" (click)="select()" [disabled]="snapshot.selectionCount === 0">Link selected assets ({{snapshot.selectionCount}})</button>
</ng-container>
</sqx-modal-dialog>

56
src/Squidex/app/shared/components/assets-selector.component.ts

@ -7,16 +7,25 @@
// tslint:disable:prefer-for-of
import { ChangeDetectionStrategy, Component, EventEmitter, OnInit, Output } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, OnInit, Output } from '@angular/core';
import { onErrorResumeNext } from 'rxjs/operators';
import {
AssetDto,
AssetsDialogState,
fadeAnimation,
LocalStoreService
LocalStoreService,
StatefulComponent
} from '@app/shared/internal';
interface State {
selectedAssets: { [id: string]: AssetDto };
selectionCount: number;
isListView: boolean;
}
@Component({
selector: 'sqx-assets-selector',
styleUrls: ['./assets-selector.component.scss'],
@ -26,32 +35,31 @@ import {
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AssetsSelectorComponent implements OnInit {
public selectedAssets: { [id: string]: AssetDto } = {};
public selectionCount = 0;
public isListView = false;
export class AssetsSelectorComponent extends StatefulComponent<State> implements OnInit {
@Output()
public selected = new EventEmitter<AssetDto[]>();
constructor(
public readonly state: AssetsDialogState,
constructor(changeDector: ChangeDetectorRef,
public readonly assetsState: AssetsDialogState,
private readonly localStore: LocalStoreService
) {
this.isListView = this.localStore.getBoolean('squidex.assets.list-view');
super(changeDector, {
selectedAssets: {},
selectionCount: 0,
isListView: localStore.getBoolean('squidex.assets.list-view')
});
}
public ngOnInit() {
this.state.load().pipe(onErrorResumeNext()).subscribe();
this.assetsState.load().pipe(onErrorResumeNext()).subscribe();
}
public reload() {
this.state.load(true).pipe(onErrorResumeNext()).subscribe();
this.assetsState.load(true).pipe(onErrorResumeNext()).subscribe();
}
public search(query: string) {
this.state.search(query).pipe(onErrorResumeNext()).subscribe();
this.assetsState.search(query).pipe(onErrorResumeNext()).subscribe();
}
public complete() {
@ -59,25 +67,31 @@ export class AssetsSelectorComponent implements OnInit {
}
public select() {
this.selected.emit(Object.values(this.selectedAssets));
this.selected.emit(Object.values(this.snapshot.selectedAssets));
}
public selectTags(tags: string[]) {
this.state.selectTags(tags).pipe(onErrorResumeNext()).subscribe();
this.assetsState.selectTags(tags).pipe(onErrorResumeNext()).subscribe();
}
public selectAsset(asset: AssetDto) {
if (this.selectedAssets[asset.id]) {
delete this.selectedAssets[asset.id];
this.next(s => {
const selectedAssets = { ...s.selectedAssets };
if (selectedAssets[asset.id]) {
delete selectedAssets[asset.id];
} else {
this.selectedAssets[asset.id] = asset;
selectedAssets[asset.id] = asset;
}
this.selectionCount = Object.keys(this.selectedAssets).length;
const selectionCount = Object.keys(selectedAssets).length;
return { ...s, selectedAssets, selectionCount };
});
}
public changeView(isListView: boolean) {
this.isListView = isListView;
this.next(s => ({ ...s, isListView }));
this.localStore.setBoolean('squidex.assets.list-view', isListView);
}

19
src/Squidex/app/shared/components/comments.component.ts

@ -7,7 +7,7 @@
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { Subscription, timer } from 'rxjs';
import { timer } from 'rxjs';
import { onErrorResumeNext, switchMap } from 'rxjs/operators';
import {
@ -17,6 +17,7 @@ import {
CommentsService,
CommentsState,
DialogService,
ResourceOwner,
UpsertCommentForm
} from '@app/shared/internal';
@ -25,9 +26,7 @@ import {
styleUrls: ['./comments.component.scss'],
templateUrl: './comments.component.html'
})
export class CommentsComponent implements OnDestroy, OnInit {
private timerSubscription: Subscription;
export class CommentsComponent extends ResourceOwner implements OnDestroy, OnInit {
public state: CommentsState;
public userId: string;
@ -43,19 +42,17 @@ export class CommentsComponent implements OnDestroy, OnInit {
private readonly dialogs: DialogService,
private readonly formBuilder: FormBuilder
) {
this.userId = authService.user!.token;
}
super();
public ngOnDestroy() {
this.timerSubscription.unsubscribe();
this.userId = authService.user!.token;
}
public ngOnInit() {
this.state = new CommentsState(this.appsState, this.commentsId, this.commentsService, this.dialogs);
this.timerSubscription =
this.takeOver(
timer(0, 4000).pipe(switchMap(() => this.state.load().pipe(onErrorResumeNext())))
.subscribe();
.subscribe());
}
public delete(comment: CommentDto) {
@ -76,7 +73,7 @@ export class CommentsComponent implements OnDestroy, OnInit {
}
}
public trackByComment(index: number, comment: CommentDto) {
public trackByComment(comment: CommentDto) {
return comment.id;
}
}

1
src/Squidex/app/shared/components/geolocation-editor.component.ts

@ -25,6 +25,7 @@ export const SQX_GEOLOCATION_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
interface Geolocation {
latitude: number;
longitude: number;
}

4
src/Squidex/app/shared/components/markdown-editor.component.ts

@ -38,8 +38,6 @@ export class MarkdownEditorComponent extends StatefulControlComponent<State, str
private value: string;
private isDisabled = false;
public assetsDialog = new DialogModel();
@ViewChild('editor')
public editor: ElementRef;
@ -49,6 +47,8 @@ export class MarkdownEditorComponent extends StatefulControlComponent<State, str
@ViewChild('inner')
public inner: ElementRef;
public assetsDialog = new DialogModel();
constructor(changeDetector: ChangeDetectorRef,
private readonly renderer: Renderer2,
private readonly resourceLoader: ResourceLoaderService

44
src/Squidex/app/shared/components/permission.directive.ts

@ -5,13 +5,14 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Directive, Input, OnChanges, TemplateRef, ViewContainerRef } from '@angular/core';
import { ChangeDetectorRef, Directive, Input, OnChanges, OnDestroy, OnInit, TemplateRef, ViewContainerRef } from '@angular/core';
import {
AppDto,
AppsState,
AuthService,
Permission,
ResourceOwner,
SchemaDto,
SchemasState
} from '@app/shared/internal';
@ -19,7 +20,7 @@ import {
@Directive({
selector: '[sqxPermission]'
})
export class PermissionDirective implements OnChanges {
export class PermissionDirective extends ResourceOwner implements OnChanges, OnInit, OnDestroy {
private viewCreated = false;
@Input('sqxPermissionApp')
@ -34,14 +35,45 @@ export class PermissionDirective implements OnChanges {
constructor(
private readonly authService: AuthService,
private readonly appsState: AppsState,
private readonly changeDetector: ChangeDetectorRef,
private readonly schemasState: SchemasState,
private readonly templateRef: TemplateRef<any>,
private readonly viewContainer: ViewContainerRef
) {
super();
}
public ngOnInit() {
this.takeOver(
this.appsState.selectedApp.subscribe(app => {
if (app && !this.app) {
this.update(app, this.schemasState.snapshot.selectedSchema);
}
}));
this.takeOver(
this.schemasState.selectedSchema.subscribe(schema => {
if (schema && !this.schema) {
this.update(this.appsState.snapshot.selectedApp, schema);
}
}));
}
public ngOnChanges() {
this.update(this.appsState.snapshot.selectedApp, this.schemasState.snapshot.selectedSchema);
}
private update(app?: AppDto | null, schema?: SchemaDto | null) {
if (this.app) {
app = this.app;
}
if (this.schema) {
schema = this.schema;
}
let permissions = this.permissions;
let show = false;
if (permissions) {
@ -54,14 +86,10 @@ export class PermissionDirective implements OnChanges {
const array = permissions.split(';');
for (let id of array) {
const app = this.app || this.appsState.snapshot.selectedApp;
if (app) {
id = id.replace('{app}', app.name);
}
const schema = this.schema || this.schemasState.snapshot.selectedSchema;
if (schema) {
id = id.replace('{name}', schema.name);
}
@ -95,9 +123,11 @@ export class PermissionDirective implements OnChanges {
if (show && !this.viewCreated) {
this.viewContainer.createEmbeddedView(this.templateRef);
this.viewCreated = true;
} else if (show && this.viewCreated) {
} else if (!show && this.viewCreated) {
this.viewContainer.clear();
this.viewCreated = false;
}
this.changeDetector.markForCheck();
}
}

6
src/Squidex/app/shared/components/rich-editor.component.ts

@ -35,14 +35,14 @@ export class RichEditorComponent extends ExternalControlComponent<string> implem
private value: string;
private isDisabled = false;
public assetsDialog = new DialogModel();
@ViewChild('editor')
public editor: ElementRef;
@Output()
public assetPluginClicked = new EventEmitter<any>();
public assetsDialog = new DialogModel();
constructor(changeDetector: ChangeDetectorRef,
private readonly resourceLoader: ResourceLoaderService
) {
@ -52,8 +52,10 @@ export class RichEditorComponent extends ExternalControlComponent<string> implem
public ngOnDestroy() {
clearTimeout(this.tinyInitTimer);
if (tinymce && this.editor) {
tinymce.remove(this.editor);
}
}
public ngAfterViewInit() {
const self = this;

12
src/Squidex/app/shared/components/schema-category.component.html

@ -1,20 +1,20 @@
<div *ngIf="!isReadonly || (schemasForCategory && schemasFiltered.length > 0)" dnd-droppable class="droppable category" [allowDrop]="allowDrop" (onDropSuccess)="changeCategory($event.dragData)">
<div *ngIf="!isReadonly || snapshot.schemasFiltered.length > 0" dnd-droppable class="droppable category" [allowDrop]="allowDrop" (onDropSuccess)="changeCategory($event.dragData)">
<div class="drop-indicator"></div>
<div class="header clearfix">
<button class="btn btn-sm btn-text-secondary" (click)="toggle()">
<i [class.icon-caret-right]="!isOpen" [class.icon-caret-down]="isOpen"></i>
<i [class.icon-caret-right]="!snapshot.isOpen" [class.icon-caret-down]="snapshot.isOpen"></i>
</button>
<h3>{{displayName}} ({{schemasFiltered.length}})</h3>
<h3>{{snapshot.displayName}} ({{snapshot.schemasFiltered.length}})</h3>
<button class="btn btn-sm btn-text-secondary float-right" *ngIf="schemasForCategory.length === 0 && !isReadonly" (click)="removing.emit()">
<button class="btn btn-sm btn-text-secondary float-right" *ngIf="snapshot.schemasForCategory.length === 0 && !isReadonly" (click)="removing.emit()">
<i class="icon-bin2"></i>
</button>
</div>
<ul class="nav nav-panel nav-dark nav-dark-bordered flex-column" *ngIf="isOpen" @fade>
<ng-container *ngFor="let schema of schemasFiltered; trackBy: trackBySchema">
<ul class="nav nav-panel nav-dark nav-dark-bordered flex-column" *ngIf="snapshot.isOpen" @fade>
<ng-container *ngFor="let schema of snapshot.schemasFiltered; trackBy: trackBySchema">
<ng-container *sqxPermission="schemaPermission(schema)">
<li class="nav-item" dnd-draggable [dragEnabled]="!isReadonly" [dragData]="schema">
<a class="nav-link" [routerLink]="schemaRoute(schema)" routerLinkActive="active">

6
src/Squidex/app/shared/components/schema-category.component.scss

@ -8,7 +8,13 @@ h3 {
}
.btn {
& {
width: 2rem;
}
&:focus {
border-color: transparent;
}
}
.category {

65
src/Squidex/app/shared/components/schema-category.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
import { onErrorResumeNext } from 'rxjs/operators';
import {
@ -15,9 +15,20 @@ import {
SchemaDetailsDto,
SchemaDto,
SchemasState,
StatefulComponent,
Types
} from '@app/shared/internal';
interface State {
displayName?: string;
schemasFiltered: ImmutableArray<SchemaDto>;
schemasForCategory: ImmutableArray<SchemaDto>;
isOpen: boolean;
}
@Component({
selector: 'sqx-schema-category',
styleUrls: ['./schema-category.component.scss'],
@ -27,7 +38,7 @@ import {
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SchemaCategoryComponent implements OnInit, OnChanges {
export class SchemaCategoryComponent extends StatefulComponent<State> implements OnInit, OnChanges {
@Output()
public removing = new EventEmitter();
@ -46,31 +57,29 @@ export class SchemaCategoryComponent implements OnInit, OnChanges {
@Input()
public schemas: ImmutableArray<SchemaDto>;
public displayName: string;
public schemasFiltered: ImmutableArray<SchemaDto>;
public schemasForCategory: ImmutableArray<SchemaDto>;
public isOpen = true;
public allowDrop = (schema: any) => {
return (Types.is(schema, SchemaDto) || Types.is(schema, SchemaDetailsDto)) && !this.isSameCategory(schema);
}
constructor(
constructor(changeDetector: ChangeDetectorRef,
private readonly localStore: LocalStoreService,
private readonly schemasState: SchemasState
) {
super(changeDetector, {
schemasFiltered: ImmutableArray.empty(),
schemasForCategory: ImmutableArray.empty(),
isOpen: true
});
}
public ngOnInit() {
this.isOpen = !this.localStore.getBoolean(this.configKey());
this.next(s => ({ ...s, isOpen: !this.localStore.getBoolean(this.configKey()) }));
}
public toggle() {
this.isOpen = !this.isOpen;
this.next(s => ({ ...s, isOpen: !s.isOpen }));
this.localStore.setBoolean(this.configKey(), !this.isOpen);
this.localStore.setBoolean(this.configKey(), !this.snapshot.isOpen);
}
public ngOnChanges(changes: SimpleChanges): void {
@ -81,22 +90,28 @@ export class SchemaCategoryComponent implements OnInit, OnChanges {
const query = this.schemasFilter;
this.schemasForCategory = this.schemas.filter(x => isSameCategory(x));
this.schemasFiltered = this.schemasForCategory.filter(x => !query || x.name.indexOf(query) >= 0);
const schemasForCategory = this.schemas.filter(x => isSameCategory(x));
const schemasFiltered = schemasForCategory.filter(x => !query || x.name.indexOf(query) >= 0);
let isOpen = false;
if (query) {
this.isOpen = true;
isOpen = true;
} else {
this.isOpen = this.localStore.get(`schema-category.${this.name}`) !== 'false';
isOpen = this.localStore.get(`schema-category.${this.name}`) !== 'false';
}
this.next(s => ({ ...s, isOpen, schemasFiltered, schemasForCategory }));
}
if (changes['name']) {
if (!this.name || this.name.length === 0) {
this.displayName = 'Schemas';
} else {
this.displayName = this.name;
let displayName = 'Schemas';
if (this.name && this.name.length > 0) {
displayName = this.name;
}
this.next(s => ({ ...s, displayName }));
}
}
@ -116,14 +131,14 @@ export class SchemaCategoryComponent implements OnInit, OnChanges {
this.schemasState.changeCategory(schema, this.name).pipe(onErrorResumeNext()).subscribe();
}
public schemaPermission(schema: SchemaDto) {
return `?squidex.apps.{app}.schemas.${schema.name}.*;squidex.apps.{app}.contents.${schema.name}.*`;
}
public trackBySchema(index: number, schema: SchemaDto) {
return schema.id;
}
public schemaPermission(schema: SchemaDto) {
return `?squidex.apps.{app}.schemas.${schema.name}.*;squidex.apps.{app}.contents.${schema.name}.*`;
}
private configKey(): string {
return `squidex.schema.category.${this.name}.closed`;
}

2
src/Squidex/app/shared/state/apps.state.ts

@ -35,7 +35,7 @@ function sameApp(lhs: AppDto, rhs?: AppDto): boolean {
@Injectable()
export class AppsState extends State<Snapshot> {
public get appName() {
return this.snapshot.selectedApp!.name;
return this.snapshot.selectedApp ? this.snapshot.selectedApp.name : '';
}
public selectedApp =

17
src/Squidex/app/shared/state/schemas.state.ts

@ -44,7 +44,6 @@ type AnyFieldDto = NestedFieldDto | RootFieldDto;
interface Snapshot {
categories: { [name: string]: boolean };
schemasApp?: string;
schemas: ImmutableArray<SchemaDto>;
isLoaded?: boolean;
@ -58,6 +57,10 @@ function sameSchema(lhs: SchemaDetailsDto | null, rhs?: SchemaDetailsDto | null)
@Injectable()
export class SchemasState extends State<Snapshot> {
public get schemaName() {
return this.snapshot.selectedSchema ? this.snapshot.selectedSchema.name : '';
}
public selectedSchema =
this.changes.pipe(map(x => x.selectedSchema),
distinctUntilChanged(sameSchema));
@ -78,10 +81,6 @@ export class SchemasState extends State<Snapshot> {
this.changes.pipe(map(x => !!x.isLoaded),
distinctUntilChanged());
public get schemaName() {
return this.snapshot.selectedSchema!.name;
}
constructor(
private readonly appsState: AppsState,
private readonly authState: AuthService,
@ -93,11 +92,11 @@ export class SchemasState extends State<Snapshot> {
public select(idOrName: string | null): Observable<SchemaDetailsDto | null> {
return this.loadSchema(idOrName).pipe(
tap(schema => {
tap(selectedSchema => {
this.next(s => {
const schemas = schema ? s.schemas.replaceBy('id', schema) : s.schemas;
const schemas = selectedSchema ? s.schemas.replaceBy('id', selectedSchema) : s.schemas;
return { ...s, selectedSchema: schema, schemas };
return { ...s, selectedSchema, schemas };
});
}));
}
@ -124,7 +123,7 @@ export class SchemasState extends State<Snapshot> {
const categories = buildCategories(s.categories, schemas);
return { ...s, schemas, schemasApp: this.appName, isLoaded: true, categories };
return { ...s, schemas, isLoaded: true, categories };
});
}),
notify(this.dialogs));

19
src/Squidex/app/shell/pages/internal/internal-area.component.ts

@ -7,13 +7,13 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';
import {
DialogService,
fadeAnimation,
LoadingService,
Notification
Notification,
ResourceOwner
} from '@app/shared';
@Component({
@ -24,24 +24,17 @@ import {
fadeAnimation
]
})
export class InternalAreaComponent implements OnDestroy, OnInit {
private queryParamsSubscription: Subscription;
public notifications: Notification[] = [];
export class InternalAreaComponent extends ResourceOwner implements OnDestroy, OnInit {
constructor(
public readonly loadingService: LoadingService,
private readonly dialogs: DialogService,
private readonly route: ActivatedRoute
) {
}
public ngOnDestroy() {
this.queryParamsSubscription.unsubscribe();
super();
}
public ngOnInit() {
this.queryParamsSubscription =
this.takeOver(
this.route.queryParams.subscribe(params => {
const successMessage = params['successMessage'];
@ -54,6 +47,6 @@ export class InternalAreaComponent implements OnDestroy, OnInit {
if (errorMessage) {
this.dialogs.notify(Notification.error(errorMessage));
}
});
}));
}
}

6
src/Squidex/app/shell/pages/internal/profile-menu.component.html

@ -2,9 +2,9 @@
<li class="nav-item dropdown">
<span class="nav-link dropdown-toggle" (click)="modalMenu.toggle()">
<span class="user">
<img class="user-picture" [attr.src]="profileId | sqxUserIdPicture" />
<img class="user-picture" [attr.src]="snapshot.profileId | sqxUserIdPicture" />
<span>{{profileDisplayName}}</span>
<span>{{snapshot.profileDisplayName}}</span>
</span>
</span>
@ -13,7 +13,7 @@
Administration
</a>
<a class="dropdown-item" [sqxPopupLink]="profileUrl">
<a class="dropdown-item" [sqxPopupLink]="snapshot.profileUrl">
Profile
</a>

47
src/Squidex/app/shell/pages/internal/profile-menu.component.ts

@ -5,17 +5,23 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { Subscription } from 'rxjs';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { filter } from 'rxjs/operators';
import {
ApiUrlConfig,
AuthService,
fadeAnimation,
ModalModel
ModalModel,
StatefulComponent
} from '@app/shared';
interface State {
profileDisplayName: string;
profileId: string;
profileUrl: string;
}
@Component({
selector: 'sqx-profile-menu',
styleUrls: ['./profile-menu.component.scss'],
@ -25,36 +31,27 @@ import {
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProfileMenuComponent implements OnDestroy, OnInit {
private authenticationSubscription: Subscription;
export class ProfileMenuComponent extends StatefulComponent<State> implements OnInit {
public modalMenu = new ModalModel();
public profileDisplayName = '';
public profileId = '';
public profileUrl = this.apiUrl.buildUrl('/identity-server/account/profile');
constructor(
private readonly authService: AuthService,
private readonly apiUrl: ApiUrlConfig,
private readonly changeDetector: ChangeDetectorRef
constructor(changeDetector: ChangeDetectorRef, apiUrl: ApiUrlConfig,
private readonly authService: AuthService
) {
super(changeDetector, {
profileDisplayName: '',
profileId: '',
profileUrl: apiUrl.buildUrl('/identity-server/account/profile')
});
}
public ngOnDestroy() {
this.authenticationSubscription.unsubscribe();
}
public ngOnInit() {
this.authenticationSubscription =
this.takeOver(
this.authService.userChanges.pipe(filter(user => !!user))
.subscribe(user => {
this.profileId = user!.id;
this.profileDisplayName = user!.displayName;
const profileId = user!.id;
const profileDisplayName = user!.displayName;
this.changeDetector.markForCheck();
});
this.next(s => ({ ...s, profileId, profileDisplayName }));
}));
}
public logout() {

Loading…
Cancel
Save