Browse Source

Improved cancellation.

pull/356/head
Sebastian Stehle 8 years ago
parent
commit
572ac6d191
  1. 2
      src/Squidex/app/features/assets/pages/assets-page.component.html
  2. 1
      src/Squidex/app/features/content/pages/content/content-page.component.html
  3. 4
      src/Squidex/app/features/content/shared/content-status.component.html
  4. 8
      src/Squidex/app/features/content/shared/content-status.component.scss
  5. 3
      src/Squidex/app/features/content/shared/content-status.component.ts
  6. 20
      src/Squidex/app/framework/utils/rxjs-extensions.ts
  7. 70
      src/Squidex/app/shared/components/asset-uploader.component.html
  8. 4
      src/Squidex/app/shared/components/asset-uploader.component.ts
  9. 6
      src/Squidex/app/shared/components/asset.component.ts
  10. 11
      src/Squidex/app/shared/components/markdown-editor.component.ts
  11. 17
      src/Squidex/app/shared/components/rich-editor.component.ts
  12. 8
      src/Squidex/app/shared/state/apps.state.ts
  13. 58
      src/Squidex/app/shared/state/asset-uploader.state.ts
  14. 8
      src/Squidex/app/shared/state/assets.state.ts
  15. 106
      src/Squidex/app/shared/state/contents.state.ts
  16. 8
      src/Squidex/app/shared/state/plans.state.ts
  17. 6
      src/Squidex/app/shared/state/rule-events.state.ts
  18. 17
      src/Squidex/app/shared/state/schemas.state.ts
  19. 8
      src/Squidex/app/shared/state/ui.state.ts

2
src/Squidex/app/features/assets/pages/assets-page.component.html

@ -7,7 +7,7 @@
<ng-container menu> <ng-container menu>
<div class="row no-gutters pl-1"> <div class="row no-gutters pl-1">
<div class="col-auto offset-xl-4"> <div class="col-auto offset-xl-2">
<sqx-shortcut keys="ctrl+shift+r" (trigger)="reload()"></sqx-shortcut> <sqx-shortcut keys="ctrl+shift+r" (trigger)="reload()"></sqx-shortcut>
<button type="button" class="btn btn-text-secondary" (click)="reload()" title="Refresh Contents (CTRL + SHIFT + R)"> <button type="button" class="btn btn-text-secondary" (click)="reload()" title="Refresh Contents (CTRL + SHIFT + R)">

1
src/Squidex/app/features/content/pages/content/content-page.component.html

@ -38,7 +38,6 @@
<button type="button" class="btn btn-outline-secondary btn-status" (click)="dropdown.toggle()" [disabled]="schema.isSingleton && !content.isPending" <button type="button" class="btn btn-outline-secondary btn-status" (click)="dropdown.toggle()" [disabled]="schema.isSingleton && !content.isPending"
[class.active]="dropdown.isOpen | async" #optionsButton> [class.active]="dropdown.isOpen | async" #optionsButton>
<sqx-content-status <sqx-content-status
[alignMiddle]="false"
[status]="content.status" [status]="content.status"
[scheduledTo]="content.scheduleJob?.status" [scheduledTo]="content.scheduleJob?.status"
[scheduledAt]="content.scheduleJob?.dueTime" [scheduledAt]="content.scheduleJob?.dueTime"

4
src/Squidex/app/features/content/shared/content-status.component.html

@ -1,11 +1,11 @@
<ng-container *ngIf="scheduledTo; else noSchedule"> <ng-container *ngIf="scheduledTo; else noSchedule">
<span class="content-status content-status-{{scheduledTo | lowercase}} mr-1" [class.middle]="alignMiddle" title="{{tooltipText}}" titlePosition="top"> <span class="content-status content-status-{{scheduledTo | lowercase}} mr-1" title="{{tooltipText}}" titlePosition="top">
<i class="icon-clock"></i> <i class="icon-clock"></i>
</span> </span>
</ng-container> </ng-container>
<ng-template #noSchedule> <ng-template #noSchedule>
<span class="content-status content-status-{{displayStatus | lowercase}} mr-1" [class.middle]="alignMiddle" title="{{tooltipText}}" titlePosition="top"> <span class="content-status content-status-{{displayStatus | lowercase}} mr-1" title="{{tooltipText}}" titlePosition="top">
<i class="icon-circle"></i> <i class="icon-circle"></i>
</span> </span>
</ng-template> </ng-template>

8
src/Squidex/app/features/content/shared/content-status.component.scss

@ -2,6 +2,10 @@
@import '_mixins'; @import '_mixins';
.content-status { .content-status {
& {
vertical-align: middle;
}
&-published { &-published {
color: $color-theme-green; color: $color-theme-green;
} }
@ -31,8 +35,4 @@
color: $color-dark-foreground; color: $color-dark-foreground;
padding: .75rem; padding: .75rem;
} }
}
.middle {
vertical-align: middle;
} }

3
src/Squidex/app/features/content/shared/content-status.component.ts

@ -31,9 +31,6 @@ export class ContentStatusComponent {
@Input() @Input()
public showLabel = false; public showLabel = false;
@Input()
public alignMiddle = true;
public get tooltipText() { public get tooltipText() {
if (this.scheduledAt) { if (this.scheduledAt) {
return `Will be set to '${this.scheduledTo}' at ${this.scheduledAt.toStringFormat('LLLL')}`; return `Will be set to '${this.scheduledTo}' at ${this.scheduledAt.toStringFormat('LLLL')}`;

20
src/Squidex/app/framework/utils/rxjs-extensions.ts

@ -7,8 +7,8 @@
// tslint:disable: only-arrow-functions // tslint:disable: only-arrow-functions
import { Observable } from 'rxjs'; import { empty, Observable } from 'rxjs';
import { map, onErrorResumeNext, shareReplay, switchMap } from 'rxjs/operators'; import { catchError, map, onErrorResumeNext, publishReplay, refCount, switchMap } from 'rxjs/operators';
import { DialogService } from './../services/dialog.service'; import { DialogService } from './../services/dialog.service';
@ -30,13 +30,17 @@ type Options<T, R = T> = { silent?: boolean, project?: ((value: T) => R) };
export function shareSubscribed<T, R = T>(dialogs: DialogService, options?: Options<T, R>) { export function shareSubscribed<T, R = T>(dialogs: DialogService, options?: Options<T, R>) {
return function mapOperation(source: Observable<T>) { return function mapOperation(source: Observable<T>) {
const shared = source.pipe(shareReplay()); const shared = source.pipe(publishReplay(), refCount());
shared.subscribe(undefined, error => { shared.pipe(
if (dialogs && (!options || !options.silent)) { catchError(error => {
dialogs.notifyError(error); if (!options || !options.silent) {
} dialogs.notifyError(error);
}); }
return empty();
}))
.subscribe();
if (options && !!options.project) { if (options && !!options.project) {
const project = options.project; const project = options.project;

70
src/Squidex/app/shared/components/asset-uploader.component.html

@ -1,42 +1,44 @@
<ul class="nav navbar-nav" *ngIf="assetUploader.uploads | async; let uploads" (sqxFileDrop)="addFiles($event)"> <ng-container *ngIf="appsState.selectedApp | async">
<li class="nav-item dropdown"> <ul class="nav navbar-nav" *ngIf="assetUploader.uploads | async; let uploads" (sqxFileDrop)="addFiles($event)">
<span class="nav-link dropdown-toggle" (click)="modalMenu.toggle()"> <li class="nav-item dropdown">
<i class="icon-upload-3"></i> <span class="nav-link dropdown-toggle" (click)="modalMenu.toggle()">
<i class="icon-upload-3"></i>
<span>{{uploads.length}}</span> <span>{{uploads.length}}</span>
</span> </span>
<div class="dropdown-menu container" *ngIf="modalMenu.isOpen | async" (sqxFileDrop)="addFiles($event)" @fade> <div class="dropdown-menu container" *ngIf="modalMenu.isOpen | async" (sqxFileDrop)="addFiles($event)" @fade>
<div class="uploads"> <div class="uploads">
<small class="uploads-empty text-muted" *ngIf="uploads.length === 0"> <small class="uploads-empty text-muted" *ngIf="uploads.length === 0">
No upload in progress, drop files here. No upload in progress, drop files here.
</small> </small>
<div class="upload row no-gutters" *ngFor="let upload of uploads; trackBy: trackByUpload"> <div class="upload row no-gutters" *ngFor="let upload of uploads; trackBy: trackByUpload">
<div class="col-auto" [ngSwitch]="upload.status"> <div class="col-auto" [ngSwitch]="upload.status">
<div *ngSwitchCase="'Failed'" class="upload-status upload-status-failed"> <div *ngSwitchCase="'Failed'" class="upload-status upload-status-failed">
<i class="icon-exclamation"></i> <i class="icon-exclamation"></i>
</div>
<div *ngSwitchCase="'Completed'" class="upload-status upload-status-success">
<i class="icon-checkmark"></i>
</div>
<div *ngSwitchDefault class="upload-status upload-status-running">
<i class="icon-hour-glass"></i>
</div>
</div> </div>
<div *ngSwitchCase="'Completed'" class="upload-status upload-status-success"> <div class="col-6">
<i class="icon-checkmark"></i> <div class="upload-name">{{upload.name}}</div>
</div> </div>
<div *ngSwitchDefault class="upload-status upload-status-running"> <div class="col">
<i class="icon-hour-glass"></i> <sqx-progress-bar [value]="upload.progress" [trailWidth]="1.5" [strokeWidth]="1.5" [showText]="false" [animated]="false"></sqx-progress-bar>
</div>
<div class="col-auto">
<button type="button" class="btn btn-text-secondary" (click)="stopUpload(upload)">
<i class="icon-close"></i>
</button>
</div> </div>
</div>
<div class="col-6">
<div class="upload-name">{{upload.name}}</div>
</div>
<div class="col">
<sqx-progress-bar [value]="upload.progress" [trailWidth]="1.5" [strokeWidth]="1.5" [showText]="false" [animated]="false"></sqx-progress-bar>
</div>
<div class="col-auto">
<button type="button" class="btn btn-text-secondary" (click)="stopUpload(upload)">
<i class="icon-close"></i>
</button>
</div> </div>
</div> </div>
</div> </div>
</div> </li>
</li> </ul>
</ul> </ng-container>

4
src/Squidex/app/shared/components/asset-uploader.component.ts

@ -8,6 +8,7 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'; import { ChangeDetectionStrategy, Component } from '@angular/core';
import { import {
AppsState,
AssetsState, AssetsState,
AssetUploaderState, AssetUploaderState,
DialogModel, DialogModel,
@ -25,9 +26,10 @@ import {
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class AssetUploaderComponent { export class AssetUploaderComponent {
public modalMenu = new DialogModel(true); public modalMenu = new DialogModel();
constructor( constructor(
public readonly appsState: AppsState,
public readonly assetUploader: AssetUploaderState, public readonly assetUploader: AssetUploaderState,
public readonly assetsState: AssetsState public readonly assetsState: AssetsState
) { ) {

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

@ -15,7 +15,7 @@ import {
StatefulComponent, StatefulComponent,
Types Types
} from '@app/shared/internal'; } from '@app/shared/internal';
import { AssetUploaderState } from '../state/asset-uploader.state'; import { AssetUploaderState, UploadCanceled } from '../state/asset-uploader.state';
interface State { interface State {
progress: number; progress: number;
@ -101,7 +101,9 @@ export class AssetComponent extends StatefulComponent<State> implements OnInit {
this.emitLoad(dto); this.emitLoad(dto);
} }
}, error => { }, error => {
this.dialogs.notifyError(error); if (!Types.is(error, UploadCanceled)) {
this.dialogs.notifyError(error);
}
this.emitLoadError(error); this.emitLoadError(error);
}); });

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

@ -10,12 +10,13 @@ import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { import {
AssetDto, AssetDto,
AssetUploaderState,
DialogModel, DialogModel,
ResourceLoaderService, ResourceLoaderService,
StatefulControlComponent, StatefulControlComponent,
Types Types,
UploadCanceled
} from '@app/shared/internal'; } from '@app/shared/internal';
import { AssetUploaderState } from '../state/asset-uploader.state';
declare var SimpleMDE: any; declare var SimpleMDE: any;
@ -245,8 +246,10 @@ export class MarkdownEditorComponent extends StatefulControlComponent<State, str
if (Types.is(asset, AssetDto)) { if (Types.is(asset, AssetDto)) {
replaceText(`![${asset.fileName}](${asset.url} '${asset.fileName}')`); replaceText(`![${asset.fileName}](${asset.url} '${asset.fileName}')`);
} }
}, () => { }, error => {
replaceText('FAILED'); if (!Types.is(error, UploadCanceled)) {
replaceText('FAILED');
}
}); });
} }
} }

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

@ -12,12 +12,13 @@ import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { import {
AssetDto, AssetDto,
AssetUploaderState,
DialogModel, DialogModel,
ResourceLoaderService, ResourceLoaderService,
StatefulControlComponent, StatefulControlComponent,
Types Types,
UploadCanceled
} from '@app/shared/internal'; } from '@app/shared/internal';
import { AssetUploaderState } from '../state/asset-uploader.state';
declare var tinymce: any; declare var tinymce: any;
@ -99,8 +100,10 @@ export class RichEditorComponent extends StatefulControlComponent<any, string> i
if (Types.is(asset, AssetDto)) { if (Types.is(asset, AssetDto)) {
success(asset.url); success(asset.url);
} }
}, () => { }, error => {
failed(); if (!Types.is(error, UploadCanceled)) {
failed();
}
}); });
}, },
@ -215,8 +218,10 @@ export class RichEditorComponent extends StatefulControlComponent<any, string> i
if (Types.is(asset, AssetDto)) { if (Types.is(asset, AssetDto)) {
replaceText(`<img src="${asset.url}" alt="${asset.fileName}" />`); replaceText(`<img src="${asset.url}" alt="${asset.fileName}" />`);
} }
}, () => { }, error => {
replaceText('FAILED'); if (!Types.is(error, UploadCanceled)) {
replaceText('FAILED');
}
}); });
} }
} }

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

@ -69,9 +69,9 @@ export class AppsState extends State<Snapshot> {
public load(): Observable<any> { public load(): Observable<any> {
return this.appsService.getApps().pipe( return this.appsService.getApps().pipe(
tap((dto: AppDto[]) => { tap(payload => {
this.next(s => { this.next(s => {
const apps = ImmutableArray.of(dto); const apps = ImmutableArray.of(payload);
return { ...s, apps }; return { ...s, apps };
}); });
@ -81,9 +81,9 @@ export class AppsState extends State<Snapshot> {
public create(request: CreateAppDto): Observable<AppDto> { public create(request: CreateAppDto): Observable<AppDto> {
return this.appsService.postApp(request).pipe( return this.appsService.postApp(request).pipe(
tap(dto => { tap(payload => {
this.next(s => { this.next(s => {
const apps = s.apps.push(dto).sortByStringAsc(x => x.name); const apps = s.apps.push(payload).sortByStringAsc(x => x.name);
return { ...s, apps }; return { ...s, apps };
}); });

58
src/Squidex/app/shared/state/asset-uploader.state.ts

@ -6,8 +6,8 @@
*/ */
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable, ReplaySubject, Subject, Subscription } from 'rxjs'; import { Observable, Subject } from 'rxjs';
import { distinctUntilChanged, map, shareReplay } from 'rxjs/operators'; import { distinctUntilChanged, map, publishReplay, refCount, takeUntil } from 'rxjs/operators';
import { import {
DateTime, DateTime,
@ -31,10 +31,7 @@ export interface Upload {
name: string; name: string;
// The upload subscription. // The upload subscription.
subscription?: Subscription; cancel: Subject<any>;
// The subject to notify subscribers.
subject: ReplaySubject<UploadResult>;
// The progress. // The progress.
progress: number; progress: number;
@ -48,6 +45,8 @@ interface Snapshot {
uploads: UploadList; uploads: UploadList;
} }
export class UploadCanceled { }
type UploadList = ImmutableArray<Upload>; type UploadList = ImmutableArray<Upload>;
type UploadResult = AssetDto | number; type UploadResult = AssetDto | number;
@ -67,10 +66,7 @@ export class AssetUploaderState extends State<Snapshot> {
} }
public stopUpload(upload: Upload) { public stopUpload(upload: Upload) {
if (upload.subscription) { upload.cancel.error(new UploadCanceled());
upload.subscription.unsubscribe();
upload.subject.complete();
}
this.next(s => { this.next(s => {
const uploads = s.uploads.removeBy('id', upload); const uploads = s.uploads.removeBy('id', upload);
@ -82,55 +78,49 @@ export class AssetUploaderState extends State<Snapshot> {
public uploadFile(file: File, target?: AssetsState, now?: DateTime): Observable<UploadResult> { public uploadFile(file: File, target?: AssetsState, now?: DateTime): Observable<UploadResult> {
const stream = this.assetsService.uploadFile(this.appName, file, this.user, now || DateTime.now()); const stream = this.assetsService.uploadFile(this.appName, file, this.user, now || DateTime.now());
return this.upload(stream, MathHelper.guid(), file, (asset, subject) => { return this.upload(stream, MathHelper.guid(), file, asset => {
if (asset.isDuplicate) { if (asset.isDuplicate) {
this.dialogs.notifyError('Asset has already been uploaded.'); this.dialogs.notifyError('Asset has already been uploaded.');
} else if (target) { } else if (target) {
target.add(asset); target.add(asset);
} }
subject.next(asset); return asset;
}); });
} }
public uploadAsset(asset: AssetDto, file: File, now?: DateTime): Observable<UploadResult> { public uploadAsset(asset: AssetDto, file: File, now?: DateTime): Observable<UploadResult> {
const stream = this.assetsService.replaceFile(this.appName, asset.id, file, asset.version); const stream = this.assetsService.replaceFile(this.appName, asset.id, file, asset.version);
return this.upload(stream, asset.id, file, ({ version, payload }, subject) => { return this.upload(stream, asset.id, file, ({ version, payload }) => asset.update(payload, this.user, version, now));
subject.next(asset.update(payload, this.user, version, now));
});
} }
private upload<T, R>(stream: Observable<number | R>, id: string, file: File, complete: (completion: R, subject: Subject<UploadResult>) => void) { private upload<T>(source: Observable<number | T>, id: string, file: File, complete: ((completion: T) => AssetDto)) {
const subject = new ReplaySubject<UploadResult>(); let upload = { id, name: file.name, progress: 1, status: 'Running', cancel: new Subject() };
let upload = { id, name: file.name, progress: 1, status: 'Running', subject };
this.addUpload(upload); this.addUpload(upload);
const subscription = stream.subscribe(event => { const stream = source.pipe(takeUntil(upload.cancel),
map(event => {
if (Types.isNumber(event)) {
return event;
} else {
return complete(event);
}
}),
publishReplay(), refCount());
stream.subscribe(event => {
if (Types.isNumber(event)) { if (Types.isNumber(event)) {
upload = this.update(upload, { progress: event }); upload = this.update(upload, { progress: event });
subject.next(event);
} else {
complete(event, subject);
} }
}, error => { }, () => {
subject.error(error);
upload = this.remove(upload, { status: 'Failed' }); upload = this.remove(upload, { status: 'Failed' });
}, () => { }, () => {
if (!subject.isStopped) {
subject.complete();
}
upload = this.remove(upload, { status: 'Completed', progress: 100 }); upload = this.remove(upload, { status: 'Completed', progress: 100 });
}); });
upload = this.update(upload, { subscription }); return stream;
return subject.pipe(shareReplay());
} }
private remove(upload: Upload, update: Partial<Upload>) { private remove(upload: Upload, update: Partial<Upload>) {

8
src/Squidex/app/shared/state/assets.state.ts

@ -95,16 +95,16 @@ export class AssetsState extends State<Snapshot> {
Object.keys(this.snapshot.tagsSelected)), Object.keys(this.snapshot.tagsSelected)),
this.assetsService.getTags(this.appName) this.assetsService.getTags(this.appName)
).pipe( ).pipe(
tap(dtos => { tap(([ { items, total }, tags ]) => {
if (isReload) { if (isReload) {
this.dialogs.notifyInfo('Assets reloaded.'); this.dialogs.notifyInfo('Assets reloaded.');
} }
this.next(s => { this.next(s => {
const assets = ImmutableArray.of(dtos[0].items); const assets = ImmutableArray.of(items);
const assetsPager = s.assetsPager.setCount(dtos[0].total); const assetsPager = s.assetsPager.setCount(total);
return { ...s, assets, assetsPager, isLoaded: true, tags: dtos[1] }; return { ...s, assets, assetsPager, isLoaded: true, tags };
}); });
}), }),
shareSubscribed(this.dialogs)); shareSubscribed(this.dialogs));

106
src/Squidex/app/shared/state/contents.state.ts

@ -14,6 +14,7 @@ import {
DialogService, DialogService,
ErrorDto, ErrorDto,
ImmutableArray, ImmutableArray,
mapVersioned,
Pager, Pager,
shareSubscribed, shareSubscribed,
State, State,
@ -118,20 +119,24 @@ export abstract class ContentsStateBase extends State<Snapshot> {
return this.loadInternal(isReload); return this.loadInternal(isReload);
} }
private loadInternal(isReload = false): Observable<any> { private loadInternal(isReload = false) {
return this.loadInternalCore(isReload).pipe(shareSubscribed(this.dialogs));
}
private loadInternalCore(isReload = false) {
return this.contentsService.getContents(this.appName, this.schemaName, return this.contentsService.getContents(this.appName, this.schemaName,
this.snapshot.contentsPager.pageSize, this.snapshot.contentsPager.pageSize,
this.snapshot.contentsPager.skip, this.snapshot.contentsPager.skip,
this.snapshot.contentsQuery, undefined, this.snapshot.contentsQuery, undefined,
this.snapshot.isArchive).pipe( this.snapshot.isArchive).pipe(
tap(dtos => { tap(({ total, items }) => {
if (isReload) { if (isReload) {
this.dialogs.notifyInfo('Contents reloaded.'); this.dialogs.notifyInfo('Contents reloaded.');
} }
return this.next(s => { return this.next(s => {
const contents = ImmutableArray.of(dtos.items); const contents = ImmutableArray.of(items);
const contentsPager = s.contentsPager.setCount(dtos.total); const contentsPager = s.contentsPager.setCount(total);
let selectedContent = s.selectedContent; let selectedContent = s.selectedContent;
@ -141,17 +146,16 @@ export abstract class ContentsStateBase extends State<Snapshot> {
return { ...s, contents, contentsPager, selectedContent, isLoaded: true }; return { ...s, contents, contentsPager, selectedContent, isLoaded: true };
}); });
}), }));
shareSubscribed(this.dialogs));
} }
public create(request: any, publish: boolean): Observable<ContentDto> { public create(request: any, publish: boolean): Observable<ContentDto> {
return this.contentsService.postContent(this.appName, this.schemaName, request, publish).pipe( return this.contentsService.postContent(this.appName, this.schemaName, request, publish).pipe(
tap(dto => { tap(payload => {
this.dialogs.notifyInfo('Contents created successfully.'); this.dialogs.notifyInfo('Contents created successfully.');
return this.next(s => { return this.next(s => {
const contents = s.contents.pushFront(dto); const contents = s.contents.pushFront(payload);
const contentsPager = s.contentsPager.incrementCount(); const contentsPager = s.contentsPager.incrementCount();
return { ...s, contents, contentsPager }; return { ...s, contents, contentsPager };
@ -174,7 +178,8 @@ export abstract class ContentsStateBase extends State<Snapshot> {
return of(error); return of(error);
}), }),
switchMap(() => this.loadInternal())); switchMap(() => this.loadInternalCore()),
shareSubscribed(this.dialogs, { silent: true }));
} }
public deleteMany(contents: ContentDto[]): Observable<any> { public deleteMany(contents: ContentDto[]): Observable<any> {
@ -191,92 +196,97 @@ export abstract class ContentsStateBase extends State<Snapshot> {
return of(error); return of(error);
}), }),
switchMap(() => this.loadInternal())); switchMap(() => this.loadInternal()),
shareSubscribed(this.dialogs, { silent: true }));
} }
public publishChanges(content: ContentDto, dueTime: string | null, now?: DateTime): Observable<any> { public publishChanges(content: ContentDto, dueTime: string | null, now?: DateTime): Observable<ContentDto> {
return this.contentsService.changeContentStatus(this.appName, this.schemaName, content.id, 'Publish', dueTime, content.version).pipe( return this.contentsService.changeContentStatus(this.appName, this.schemaName, content.id, 'Publish', dueTime, content.version).pipe(
tap(dto => { mapVersioned(({ version }) => {
this.dialogs.notifyInfo('Content updated successfully.');
if (dueTime) { if (dueTime) {
this.replaceContent(changeScheduleStatus(content, 'Published', dueTime, this.user, dto.version, now)); return changeScheduleStatus(content, 'Published', dueTime, this.user, version, now);
} else { } else {
this.replaceContent(confirmChanges(content, this.user, dto.version, now)); return confirmChanges(content, this.user, version, now);
} }
}), }),
tap(({ payload }) => {
this.dialogs.notifyInfo('Content updated successfully.');
this.replaceContent(payload);
}),
shareSubscribed(this.dialogs)); shareSubscribed(this.dialogs));
} }
public changeStatus(content: ContentDto, action: string, status: string, dueTime: string | null, now?: DateTime): Observable<any> { public changeStatus(content: ContentDto, action: string, status: string, dueTime: string | null, now?: DateTime): Observable<ContentDto> {
return this.contentsService.changeContentStatus(this.appName, this.schemaName, content.id, action, dueTime, content.version).pipe( return this.contentsService.changeContentStatus(this.appName, this.schemaName, content.id, action, dueTime, content.version).pipe(
tap(dto => { mapVersioned(({ version }) => {
this.dialogs.notifyInfo('Content updated successfully.');
if (dueTime) { if (dueTime) {
this.replaceContent(changeScheduleStatus(content, status, dueTime, this.user, dto.version, now)); return changeScheduleStatus(content, status, dueTime, this.user, version, now);
} else { } else {
this.replaceContent(changeStatus(content, status, this.user, dto.version, now)); return changeStatus(content, status, this.user, version, now);
} }
}), }),
tap(({ payload }) => {
this.dialogs.notifyInfo('Content updated successfully.');
this.replaceContent(payload);
}),
shareSubscribed(this.dialogs)); shareSubscribed(this.dialogs));
} }
public update(content: ContentDto, request: any, now?: DateTime): Observable<any> { public update(content: ContentDto, request: any, now?: DateTime): Observable<ContentDto> {
return this.contentsService.putContent(this.appName, this.schemaName, content.id, request, false, content.version).pipe( return this.contentsService.putContent(this.appName, this.schemaName, content.id, request, false, content.version).pipe(
tap(dto => { mapVersioned((payload, version) => updateData(content, payload, this.user, version, now)),
tap(({ payload }) => {
this.dialogs.notifyInfo('Content updated successfully.'); this.dialogs.notifyInfo('Content updated successfully.');
if (dto.version.value !== content.version.value) { this.replaceContent(payload, content.version);
this.replaceContent(updateData(content, dto.payload, this.user, dto.version, now));
}
}), }),
shareSubscribed(this.dialogs)); shareSubscribed(this.dialogs));
} }
public proposeUpdate(content: ContentDto, request: any, now?: DateTime): Observable<any> { public proposeUpdate(content: ContentDto, request: any, now?: DateTime): Observable<ContentDto> {
return this.contentsService.putContent(this.appName, this.schemaName, content.id, request, true, content.version).pipe( return this.contentsService.putContent(this.appName, this.schemaName, content.id, request, true, content.version).pipe(
tap(dto => { mapVersioned((payload, version) => updateDataDraft(content, payload, this.user, version, now)),
tap(({ payload }) => {
this.dialogs.notifyInfo('Content updated successfully.'); this.dialogs.notifyInfo('Content updated successfully.');
if (dto.version.value !== content.version.value) { this.replaceContent(payload, content.version);
this.replaceContent(updateDataDraft(content, dto.payload, this.user, dto.version, now));
}
}), }),
shareSubscribed(this.dialogs)); shareSubscribed(this.dialogs));
} }
public discardChanges(content: ContentDto, now?: DateTime): Observable<any> { public discardChanges(content: ContentDto, now?: DateTime): Observable<ContentDto> {
return this.contentsService.discardChanges(this.appName, this.schemaName, content.id, content.version).pipe( return this.contentsService.discardChanges(this.appName, this.schemaName, content.id, content.version).pipe(
tap(dto => { mapVersioned((_, version) => discardChanges(content, this.user, version, now)),
tap(({ payload }) => {
this.dialogs.notifyInfo('Content updated successfully.'); this.dialogs.notifyInfo('Content updated successfully.');
if (dto.version.value !== content.version.value) { this.replaceContent(payload, content.version);
this.replaceContent(discardChanges(content, this.user, dto.version, now));
}
}), }),
shareSubscribed(this.dialogs)); shareSubscribed(this.dialogs));
} }
public patch(content: ContentDto, request: any, now?: DateTime): Observable<any> { public patch(content: ContentDto, request: any, now?: DateTime): Observable<ContentDto> {
return this.contentsService.patchContent(this.appName, this.schemaName, content.id, request, content.version).pipe( return this.contentsService.patchContent(this.appName, this.schemaName, content.id, request, content.version).pipe(
tap(dto => { mapVersioned((data, version) => updateData(content, data, this.user, version, now)),
tap(({ payload }) => {
this.dialogs.notifyInfo('Content updated successfully.'); this.dialogs.notifyInfo('Content updated successfully.');
if (dto.version.value !== content.version.value) { this.replaceContent(payload, content.version);
this.replaceContent(updateData(content, dto.payload, this.user, dto.version, now));
}
}), }),
shareSubscribed(this.dialogs)); shareSubscribed(this.dialogs));
} }
private replaceContent(content: ContentDto) { private replaceContent(content: ContentDto, oldVersion?: Version) {
return this.next(s => { if (!oldVersion || oldVersion.eq(content.version)) {
const contents = s.contents.replaceBy('id', content); return this.next(s => {
const selectedContent = s.selectedContent && s.selectedContent.id === content.id ? content : s.selectedContent; const contents = s.contents.replaceBy('id', content);
const selectedContent = s.selectedContent && s.selectedContent.id === content.id ? content : s.selectedContent;
return { ...s, contents, selectedContent }; return { ...s, contents, selectedContent };
}); });
}
} }
public goArchive(isArchive: boolean): Observable<any> { public goArchive(isArchive: boolean): Observable<any> {

8
src/Squidex/app/shared/state/plans.state.ts

@ -112,14 +112,14 @@ export class PlansState extends State<Snapshot> {
public change(planId: string): Observable<any> { public change(planId: string): Observable<any> {
return this.plansService.putPlan(this.appName, { planId }, this.version).pipe( return this.plansService.putPlan(this.appName, { planId }, this.version).pipe(
tap(dto => { tap(({ payload, version }) => {
if (dto.payload.redirectUri && dto.payload.redirectUri.length > 0) { if (payload.redirectUri && payload.redirectUri.length > 0) {
this.window.location.href = dto.payload.redirectUri; this.window.location.href = payload.redirectUri;
} else { } else {
this.next(s => { this.next(s => {
const plans = s.plans.map(x => this.createPlan(x.plan, planId)); const plans = s.plans.map(x => this.createPlan(x.plan, planId));
return { ...s, plans, isOwner: true, version: dto.version }; return { ...s, plans, isOwner: true, version };
}); });
} }
}), }),

6
src/Squidex/app/shared/state/rule-events.state.ts

@ -66,14 +66,14 @@ export class RuleEventsState extends State<Snapshot> {
return this.rulesService.getEvents(this.appName, return this.rulesService.getEvents(this.appName,
this.snapshot.ruleEventsPager.pageSize, this.snapshot.ruleEventsPager.pageSize,
this.snapshot.ruleEventsPager.skip).pipe( this.snapshot.ruleEventsPager.skip).pipe(
tap(dtos => { tap(({ total, items }) => {
if (isReload) { if (isReload) {
this.dialogs.notifyInfo('RuleEvents reloaded.'); this.dialogs.notifyInfo('RuleEvents reloaded.');
} }
return this.next(s => { return this.next(s => {
const ruleEvents = ImmutableArray.of(dtos.items); const ruleEvents = ImmutableArray.of(items);
const ruleEventsPager = s.ruleEventsPager.setCount(dtos.total); const ruleEventsPager = s.ruleEventsPager.setCount(total);
return { ...s, ruleEvents, ruleEventsPager, isLoaded: true }; return { ...s, ruleEvents, ruleEventsPager, isLoaded: true };
}); });

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

@ -93,7 +93,7 @@ export class SchemasState extends State<Snapshot> {
private readonly dialogs: DialogService, private readonly dialogs: DialogService,
private readonly schemasService: SchemasService private readonly schemasService: SchemasService
) { ) {
super({ schemas: ImmutableArray.empty(), categories: {} }); super({ schemas: ImmutableArray.empty(), categories: buildCategories({}) });
} }
public select(idOrName: string | null): Observable<SchemaDetailsDto | null> { public select(idOrName: string | null): Observable<SchemaDetailsDto | null> {
@ -137,9 +137,9 @@ export class SchemasState extends State<Snapshot> {
public create(request: CreateSchemaDto, now?: DateTime): Observable<SchemaDetailsDto> { public create(request: CreateSchemaDto, now?: DateTime): Observable<SchemaDetailsDto> {
return this.schemasService.postSchema(this.appName, request, this.user, now || DateTime.now()).pipe( return this.schemasService.postSchema(this.appName, request, this.user, now || DateTime.now()).pipe(
tap(dto => { tap(payload => {
this.next(s => { this.next(s => {
const schemas = s.schemas.push(dto).sortByStringAsc(x => x.displayName); const schemas = s.schemas.push(payload).sortByStringAsc(x => x.displayName);
return { ...s, schemas }; return { ...s, schemas };
}); });
@ -375,7 +375,7 @@ export class SchemasState extends State<Snapshot> {
} }
} }
function buildCategories(categories: { [name: string]: boolean }, schemas: SchemasList) { function buildCategories(categories: { [name: string]: boolean }, schemas?: SchemasList) {
categories = { ...categories }; categories = { ...categories };
for (let category in categories) { for (let category in categories) {
@ -385,10 +385,15 @@ function buildCategories(categories: { [name: string]: boolean }, schemas: Schem
} }
} }
} }
for (let schema of schemas.values) {
categories[schema.category || ''] = false; if (schemas) {
for (let schema of schemas.values) {
categories[schema.category || ''] = false;
}
} }
categories[''] = true;
return categories; return categories;
} }

8
src/Squidex/app/shared/state/ui.state.ts

@ -55,15 +55,15 @@ export class UIState extends State<Snapshot> {
this.next(s => updateAppSettings(s, {})); this.next(s => updateAppSettings(s, {}));
this.uiService.getSettings(this.appName) this.uiService.getSettings(this.appName)
.subscribe(dtos => { .subscribe(payload => {
this.next(s => updateAppSettings(s, dtos)); this.next(s => updateAppSettings(s, payload));
}); });
} }
private loadCommon() { private loadCommon() {
this.uiService.getCommonSettings() this.uiService.getCommonSettings()
.subscribe(dtos => { .subscribe(payload => {
this.next(s => updateCommonSettings(s, dtos)); this.next(s => updateCommonSettings(s, payload));
}); });
} }

Loading…
Cancel
Save