Browse Source

Using uploader

pull/356/head
Sebastian Stehle 8 years ago
parent
commit
4b99c96fa7
  1. 14
      src/Squidex/app/framework/utils/rxjs-extensions.ts
  2. 2
      src/Squidex/app/shared/components/asset-uploader.component.html
  3. 17
      src/Squidex/app/shared/components/asset-uploader.component.ts
  4. 14
      src/Squidex/app/shared/components/asset.component.ts
  5. 11
      src/Squidex/app/shared/components/markdown-editor.component.ts
  6. 23
      src/Squidex/app/shared/components/rich-editor.component.ts
  7. 1
      src/Squidex/app/shared/internal.ts
  8. 6
      src/Squidex/app/shared/module.ts
  9. 181
      src/Squidex/app/shared/state/asset-uploader.state.ts
  10. 180
      src/Squidex/app/shared/state/assets.state.spec.ts
  11. 21
      src/Squidex/app/shared/state/clients.state.spec.ts
  12. 3
      src/Squidex/app/shared/state/contents.state.ts
  13. 12
      src/Squidex/app/shared/state/contributors.state.ts

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

@ -7,8 +7,8 @@
// tslint:disable: only-arrow-functions // tslint:disable: only-arrow-functions
import { Observable, throwError } from 'rxjs'; import { Observable } from 'rxjs';
import { catchError, map, onErrorResumeNext, shareReplay, switchMap } from 'rxjs/operators'; import { map, onErrorResumeNext, shareReplay, switchMap } from 'rxjs/operators';
import { DialogService } from './../services/dialog.service'; import { DialogService } from './../services/dialog.service';
@ -26,16 +26,6 @@ export function mapVersioned<T = any, R = any>(project: (value: T, version: Vers
}; };
} }
export function notify<T>(dialogs: DialogService) {
return function mapOperation(source: Observable<T>) {
return source.pipe(catchError(error => {
dialogs.notifyError(error);
return throwError(error);
}));
};
}
type Options<T, R = T> = { silent?: boolean, project?: ((value: T) => R) }; 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>) {

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

@ -1,4 +1,4 @@
<ul class="nav navbar-nav" *ngIf="assets.uploadsDelayed | async; let uploads" (sqxFileDrop)="addFiles($event)"> <ul class="nav navbar-nav" *ngIf="assetUploader.uploads | async; let uploads" (sqxFileDrop)="addFiles($event)">
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<span class="nav-link dropdown-toggle" (click)="modalMenu.toggle()"> <span class="nav-link dropdown-toggle" (click)="modalMenu.toggle()">
<i class="icon-upload-3"></i> <i class="icon-upload-3"></i>

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

@ -9,8 +9,10 @@ import { ChangeDetectionStrategy, Component } from '@angular/core';
import { import {
AssetsState, AssetsState,
AssetUploaderState,
DialogModel, DialogModel,
fadeAnimation fadeAnimation,
Upload
} from '@app/shared/internal'; } from '@app/shared/internal';
@Component({ @Component({
@ -26,11 +28,20 @@ export class AssetUploaderComponent {
public modalMenu = new DialogModel(true); public modalMenu = new DialogModel(true);
constructor( constructor(
public readonly assets: AssetsState public readonly assetUploader: AssetUploaderState,
public readonly assetsState: AssetsState
) { ) {
} }
public addFiles() { public addFiles(files: File[]) {
for (let file of files) {
this.assetUploader.uploadFile(file, this.assetsState);
}
this.modalMenu.show(); this.modalMenu.show();
} }
public stopUpload(upload: Upload) {
this.assetUploader.stopUpload(upload);
}
} }

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

@ -8,17 +8,15 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, HostBinding, Input, OnInit, Output } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, HostBinding, Input, OnInit, Output } from '@angular/core';
import { import {
AppsState,
AssetDto, AssetDto,
AssetsService,
AuthService, AuthService,
DateTime,
DialogModel, DialogModel,
DialogService, DialogService,
fadeAnimation, fadeAnimation,
StatefulComponent, StatefulComponent,
Types Types
} from '@app/shared/internal'; } from '@app/shared/internal';
import { AssetUploaderState } from '../state/asset-uploader.state';
interface State { interface State {
progress: number; progress: number;
@ -82,9 +80,7 @@ export class AssetComponent extends StatefulComponent<State> implements OnInit {
public editDialog = new DialogModel(); public editDialog = new DialogModel();
constructor(changeDetector: ChangeDetectorRef, constructor(changeDetector: ChangeDetectorRef,
private readonly appsState: AppsState, private readonly assetUploader: AssetUploaderState,
private readonly assetsService: AssetsService,
private readonly authState: AuthService,
private readonly dialogs: DialogService private readonly dialogs: DialogService
) { ) {
super(changeDetector, { super(changeDetector, {
@ -98,7 +94,7 @@ export class AssetComponent extends StatefulComponent<State> implements OnInit {
if (initFile) { if (initFile) {
this.setProgress(1); this.setProgress(1);
this.assetsService.uploadFile(this.appsState.appName, initFile, this.authState.user!.token, DateTime.now()) this.assetUploader.uploadFile(initFile)
.subscribe(dto => { .subscribe(dto => {
if (Types.isNumber(dto)) { if (Types.isNumber(dto)) {
this.setProgress(dto); this.setProgress(dto);
@ -117,12 +113,12 @@ export class AssetComponent extends StatefulComponent<State> implements OnInit {
if (files.length === 1) { if (files.length === 1) {
this.setProgress(1); this.setProgress(1);
this.assetsService.replaceFile(this.appsState.appName, this.asset.id, files[0], this.asset.version) this.assetUploader.uploadUpdate(this.asset, files[0])
.subscribe(dto => { .subscribe(dto => {
if (Types.isNumber(dto)) { if (Types.isNumber(dto)) {
this.setProgress(dto); this.setProgress(dto);
} else { } else {
this.updateAsset(this.asset.update(dto.payload, this.authState.user!.token, dto.version), true); this.updateAsset(dto, true);
} }
}, error => { }, error => {
this.dialogs.notifyError(error); this.dialogs.notifyError(error);

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

@ -9,16 +9,13 @@ import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, E
import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { import {
AppsState,
AssetDto, AssetDto,
AssetsService,
AuthService,
DateTime,
DialogModel, DialogModel,
ResourceLoaderService, ResourceLoaderService,
StatefulControlComponent, StatefulControlComponent,
Types Types
} from '@app/shared/internal'; } from '@app/shared/internal';
import { AssetUploaderState } from '../state/asset-uploader.state';
declare var SimpleMDE: any; declare var SimpleMDE: any;
@ -54,9 +51,7 @@ export class MarkdownEditorComponent extends StatefulControlComponent<State, str
public assetsDialog = new DialogModel(); public assetsDialog = new DialogModel();
constructor(changeDetector: ChangeDetectorRef, constructor(changeDetector: ChangeDetectorRef,
private readonly appsState: AppsState, private readonly assetUploader: AssetUploaderState,
private readonly assetsService: AssetsService,
private readonly authState: AuthService,
private readonly renderer: Renderer2, private readonly renderer: Renderer2,
private readonly resourceLoader: ResourceLoaderService private readonly resourceLoader: ResourceLoaderService
) { ) {
@ -245,7 +240,7 @@ export class MarkdownEditorComponent extends StatefulControlComponent<State, str
} }
}; };
this.assetsService.uploadFile(this.appsState.appName, file, this.authState.user!.token, DateTime.now()) this.assetUploader.uploadFile(file)
.subscribe(asset => { .subscribe(asset => {
if (Types.is(asset, AssetDto)) { if (Types.is(asset, AssetDto)) {
replaceText(`![${asset.fileName}](${asset.url} '${asset.fileName}')`); replaceText(`![${asset.fileName}](${asset.url} '${asset.fileName}')`);

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

@ -11,16 +11,13 @@ import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, E
import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { import {
AppsState,
AssetDto, AssetDto,
AssetsService,
AuthService,
DateTime,
DialogModel, DialogModel,
ResourceLoaderService, ResourceLoaderService,
StatefulControlComponent, StatefulControlComponent,
Types Types
} from '@app/shared/internal'; } from '@app/shared/internal';
import { AssetUploaderState } from '../state/asset-uploader.state';
declare var tinymce: any; declare var tinymce: any;
@ -57,9 +54,7 @@ export class RichEditorComponent extends StatefulControlComponent<any, string> i
public assetsDialog = new DialogModel(); public assetsDialog = new DialogModel();
constructor(changeDetector: ChangeDetectorRef, constructor(changeDetector: ChangeDetectorRef,
private readonly appsState: AppsState, private readonly assetUploader: AssetUploaderState,
private readonly assetsService: AssetsService,
private readonly authState: AuthService,
private readonly resourceLoader: ResourceLoaderService private readonly resourceLoader: ResourceLoaderService
) { ) {
super(changeDetector, {}); super(changeDetector, {});
@ -99,7 +94,7 @@ export class RichEditorComponent extends StatefulControlComponent<any, string> i
images_upload_handler: (blob: any, success: (url: string) => void, failed: () => void) => { images_upload_handler: (blob: any, success: (url: string) => void, failed: () => void) => {
const file = new File([blob.blob()], blob.filename(), { lastModified: new Date().getTime() }); const file = new File([blob.blob()], blob.filename(), { lastModified: new Date().getTime() });
this.assetsService.uploadFile(this.appsState.appName, file, this.authState.user!.token, DateTime.now()) this.assetUploader.uploadFile(file)
.subscribe(asset => { .subscribe(asset => {
if (Types.is(asset, AssetDto)) { if (Types.is(asset, AssetDto)) {
success(asset.url); success(asset.url);
@ -131,11 +126,13 @@ export class RichEditorComponent extends StatefulControlComponent<any, string> i
}); });
self.tinyEditor.on('paste', (event: ClipboardEvent) => { self.tinyEditor.on('paste', (event: ClipboardEvent) => {
for (let i = 0; i < event.clipboardData.items.length; i++) { if (event.clipboardData) {
const file = event.clipboardData.items[i].getAsFile(); for (let i = 0; i < event.clipboardData.items.length; i++) {
const file = event.clipboardData.items[i].getAsFile();
if (file && ImageTypes.indexOf(file.type) >= 0) { if (file && ImageTypes.indexOf(file.type) >= 0) {
self.uploadFile(file); self.uploadFile(file);
}
} }
} }
}); });
@ -213,7 +210,7 @@ export class RichEditorComponent extends StatefulControlComponent<any, string> i
this.tinyEditor.setContent(content); this.tinyEditor.setContent(content);
}; };
this.assetsService.uploadFile(this.appsState.appName, file, this.authState.user!.token, DateTime.now()) this.assetUploader.uploadFile(file)
.subscribe(asset => { .subscribe(asset => {
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}" />`);

1
src/Squidex/app/shared/internal.ts

@ -47,6 +47,7 @@ export * from './services/users.service';
export * from './state/apps.forms'; export * from './state/apps.forms';
export * from './state/apps.state'; export * from './state/apps.state';
export * from './state/asset-uploader.state';
export * from './state/assets.forms'; export * from './state/assets.forms';
export * from './state/assets.state'; export * from './state/assets.state';
export * from './state/backups.forms'; export * from './state/backups.forms';

6
src/Squidex/app/shared/module.ts

@ -21,11 +21,13 @@ import {
AssetComponent, AssetComponent,
AssetDialogComponent, AssetDialogComponent,
AssetPreviewUrlPipe, AssetPreviewUrlPipe,
AssetsDialogState,
AssetsListComponent, AssetsListComponent,
AssetsSelectorComponent, AssetsSelectorComponent,
AssetsService, AssetsService,
AssetsState, AssetsState,
AssetUploaderComponent, AssetUploaderComponent,
AssetUploaderState,
AssetUrlPipe, AssetUrlPipe,
AuthInterceptor, AuthInterceptor,
AuthService, AuthService,
@ -165,6 +167,9 @@ import {
UserPicturePipe, UserPicturePipe,
UserPictureRefPipe, UserPictureRefPipe,
TableHeaderComponent TableHeaderComponent
],
providers: [
AssetsDialogState
] ]
}) })
export class SqxSharedModule { export class SqxSharedModule {
@ -182,6 +187,7 @@ export class SqxSharedModule {
AppsState, AppsState,
AssetsState, AssetsState,
AssetsService, AssetsService,
AssetUploaderState,
AuthService, AuthService,
BackupsService, BackupsService,
BackupsState, BackupsState,

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

@ -0,0 +1,181 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Injectable } from '@angular/core';
import { Observable, Subject, Subscription } from 'rxjs';
import { distinctUntilChanged, map } from 'rxjs/operators';
import {
DateTime,
DialogService,
ImmutableArray,
MathHelper,
State,
Types
} from '@app/framework';
import { AuthService } from '../services/auth.service';
import { AssetDto, AssetsService } from './../services/assets.service';
import { AppsState } from './apps.state';
import { AssetsState } from './assets.state';
export interface Upload {
// Unique id.
id: string;
// The name of the asset.
name: string;
// The upload subscription.
subscription: Subscription;
// The progress.
progress: number;
// The status.
status: string;
}
interface Snapshot {
// The uploads.
uploads: UploadList;
}
type UploadList = ImmutableArray<Upload>;
type UploadResult = AssetDto | number;
@Injectable()
export class AssetUploaderState extends State<Snapshot> {
public uploads =
this.changes.pipe(map(x => x.uploads),
distinctUntilChanged());
constructor(
private readonly appsState: AppsState,
private readonly assetsService: AssetsService,
private readonly authService: AuthService,
private readonly dialogs: DialogService
) {
super({ uploads: ImmutableArray.empty() });
}
public stopUpload(upload: Upload) {
upload.subscription.unsubscribe();
this.next(s => {
const uploads = s.uploads.removeBy('id', upload);
return { ...s, uploads };
});
}
public uploadFile(file: File, target?: AssetsState, now?: DateTime): Observable<UploadResult> {
const observable = this.assetsService.uploadFile(this.appName, file, this.user, now || DateTime.now());
let upload: Upload;
const subject = new Subject<UploadResult>();
const subscription = observable.subscribe(event => {
if (Types.isNumber(event)) {
this.update(upload, { progress: event });
} else {
if (event.isDuplicate) {
this.dialogs.notifyError('Asset has already been uploaded.');
}
if (target) {
target.add(event);
}
}
subject.next(event);
}, error => {
subject.error(error);
this.remove(upload, 'failed');
}, () => {
subject.complete();
this.remove(upload, 'completed');
});
upload = { id: MathHelper.guid(), name: file.name, progress: 1, subscription, status: 'running' };
this.addUpload(upload);
return subject;
}
public uploadUpdate(asset: AssetDto, file: File, now?: DateTime): Observable<UploadResult> {
const observable = this.assetsService.replaceFile(this.appName, asset.id, file, asset.version);
let upload: Upload;
const subject = new Subject<UploadResult>();
const subscription = observable.subscribe(event => {
if (Types.isNumber(event)) {
this.update(upload, { progress: event });
subject.next(event);
} else {
subject.next(asset.update(event.payload, this.user, event.version, now));
}
}, error => {
subject.error(error);
this.remove(upload, 'failed');
}, () => {
subject.complete();
this.remove(upload, 'completed');
});
upload = { id: asset.id, name: file.name, progress: 1, subscription, status: 'running' };
this.addUpload(upload);
return subject;
}
private remove(upload: Upload, status: string) {
this.update(upload, { status });
setTimeout(() => {
this.next(s => {
const uploads = s.uploads.removeBy('id', upload);
return { ...s, uploads };
});
}, 10000);
}
private update(upload: Upload, update: Partial<Upload>) {
this.next(s => {
const uploads = s.uploads.replaceBy('id', { ...upload, ...update });
return { ...s, uploads };
});
}
private addUpload(upload: Upload) {
this.next(s => {
const uploads = s.uploads.push(upload);
return { ...s, uploads };
});
}
private get appName() {
return this.appsState.appName;
}
private get user() {
return this.authService.user!.token;
}
}

180
src/Squidex/app/shared/state/assets.state.spec.ts

@ -36,6 +36,8 @@ describe('AssetsState', () => {
new AssetDto('id2', creator, creator, creation, creation, 'name2', 'hash2', 'type2', 2, 2, 'mime2', false, false, null, null, 'slug2', ['tag2', 'shared'], 'url2', version) new AssetDto('id2', creator, creator, creation, creation, 'name2', 'hash2', 'type2', 2, 2, 'mime2', false, false, null, null, 'slug2', ['tag2', 'shared'], 'url2', version)
]; ];
const newAsset = new AssetDto('id1', modifier, modifier, modified, modified, 'name3', 'hash3', 'type3', 3, 3, 'mime3', false, true, 0, 0, 'slug3', ['new'], 'url3', version);
let dialogs: IMock<DialogService>; let dialogs: IMock<DialogService>;
let assetsService: IMock<AssetsService>; let assetsService: IMock<AssetsService>;
let assetsState: AssetsState; let assetsState: AssetsState;
@ -44,123 +46,147 @@ describe('AssetsState', () => {
dialogs = Mock.ofType<DialogService>(); dialogs = Mock.ofType<DialogService>();
assetsService = Mock.ofType<AssetsService>(); assetsService = Mock.ofType<AssetsService>();
assetsState = new AssetsState(appsState.object, assetsService.object, dialogs.object);
});
assetsService.setup(x => x.getAssets(app, 30, 0, undefined, [])) afterEach(() => {
.returns(() => of(new AssetsDto(200, oldAssets))); assetsService.verifyAll();
});
assetsService.setup(x => x.getTags(app)) describe('Loading', () => {
.returns(() => of({ tag1: 1, shared: 2, tag2: 1 })); it('should load assets', () => {
assetsService.setup(x => x.getAssets(app, 30, 0, undefined, []))
.returns(() => of(new AssetsDto(200, oldAssets))).verifiable();
assetsState = new AssetsState(appsState.object, assetsService.object, dialogs.object); assetsService.setup(x => x.getTags(app))
assetsState.load().subscribe(); .returns(() => of({ tag1: 1, shared: 2, tag2: 1 })).verifiable();
});
it('should load assets', () => { assetsState.load().subscribe();
assetsState.load().subscribe();
expect(assetsState.snapshot.assets.values).toEqual(oldAssets); expect(assetsState.snapshot.assets.values).toEqual(oldAssets);
expect(assetsState.snapshot.assetsPager.numberOfItems).toEqual(200); expect(assetsState.snapshot.assetsPager.numberOfItems).toEqual(200);
expect(assetsState.snapshot.isLoaded).toBeTruthy(); expect(assetsState.snapshot.isLoaded).toBeTruthy();
assetsService.verify(x => x.getAssets(app, 30, 0, undefined, []), Times.exactly(2)); dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never());
assetsService.verify(x => x.getTags(app), Times.exactly(2)); });
dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never()); it('should show notification on load when reload is true', () => {
}); assetsService.setup(x => x.getAssets(app, 30, 0, undefined, []))
.returns(() => of(new AssetsDto(200, oldAssets)));
it('should show notification on load when reload is true', () => { assetsService.setup(x => x.getTags(app))
assetsState.load(true).subscribe(); .returns(() => of({ tag1: 1, shared: 2, tag2: 1 })).verifiable();
expect().nothing(); assetsState.load(true).subscribe();
dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once()); expect().nothing();
});
it('should add asset to snapshot when created', () => { dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once());
const newAsset = new AssetDto('id3', creator, creator, creation, creation, 'name3', 'hash3', 'type3', 3, 3, 'mime3', false, true, 0, 0, 'slug2', [], 'url3', version); });
assetsState.add(newAsset); it('should load with tags when tag toggled', () => {
assetsService.setup(x => x.getAssets(app, 30, 0, undefined, ['tag1']))
.returns(() => of(new AssetsDto(0, [])));
expect(assetsState.snapshot.assets.values).toEqual([newAsset, ...oldAssets]); assetsState.toggleTag('tag1').subscribe();
expect(assetsState.snapshot.assetsPager.numberOfItems).toBe(201);
});
it('should update properties when updated', () => { expect(assetsState.isTagSelected('tag1')).toBeTruthy();
const newAsset = new AssetDto('id1', modifier, modifier, modified, modified, 'name3', 'hash3', 'type3', 3, 3, 'mime3', false, true, 0, 0, 'slug3', ['new'], 'url3', version); });
assetsState.update(newAsset); it('should load without tags when tag toggled', () => {
assetsService.setup(x => x.getAssets(app, 30, 0, undefined, ['tag1']))
.returns(() => of(new AssetsDto(0, [])));
const asset_1 = assetsState.snapshot.assets.at(0); assetsService.setup(x => x.getAssets(app, 30, 0, undefined, []))
.returns(() => of(new AssetsDto(0, [])));
expect(asset_1).toBe(newAsset); assetsState.toggleTag('tag1').subscribe();
expect(assetsState.snapshot.tags).toEqual({ tag2: 1, shared: 1, new: 1 }); assetsState.toggleTag('tag1').subscribe();
});
it('should remove asset from snapshot when deleted', () => { expect(assetsState.isTagSelected('tag1')).toBeFalsy();
assetsService.setup(x => x.deleteAsset(app, oldAssets[0].id, version)) });
.returns(() => of(versioned(newVersion)));
assetsState.delete(oldAssets[0]).subscribe(); it('should load with tags when tags selected', () => {
assetsService.setup(x => x.getAssets(app, 30, 0, undefined, ['tag1', 'tag2']))
.returns(() => of(new AssetsDto(0, [])));
expect(assetsState.snapshot.assets.values.length).toBe(1); assetsState.selectTags(['tag1', 'tag2']).subscribe();
expect(assetsState.snapshot.assetsPager.numberOfItems).toBe(199);
expect(assetsState.snapshot.tags).toEqual({ shared: 1, tag2: 1 });
});
it('should load next page and prev page when paging', () => { expect(assetsState.isTagSelected('tag1')).toBeTruthy();
assetsService.setup(x => x.getAssets(app, 30, 30, undefined, [])) });
.returns(() => of(new AssetsDto(200, [])));
assetsState.goNext().subscribe(); it('should load without tags when tags reset', () => {
assetsState.goPrev().subscribe(); assetsService.setup(x => x.getAssets(app, 30, 0, undefined, []))
.returns(() => of(new AssetsDto(0, [])));
expect().nothing(); assetsState.resetTags().subscribe();
assetsService.verify(x => x.getAssets(app, 30, 30, undefined, []), Times.once()); expect(assetsState.isTagSelectionEmpty()).toBeTruthy();
assetsService.verify(x => x.getAssets(app, 30, 0, undefined, []), Times.exactly(2)); });
});
it('should load next page and prev page when paging', () => {
assetsService.setup(x => x.getAssets(app, 30, 30, undefined, []))
.returns(() => of(new AssetsDto(200, [])));
it('should load with query when searching', () => { assetsState.goNext().subscribe();
assetsService.setup(x => x.getAssets(app, 30, 0, 'my-query', [])) assetsState.goPrev().subscribe();
.returns(() => of(new AssetsDto(0, [])));
assetsState.search('my-query').subscribe(); expect().nothing();
});
expect(assetsState.snapshot.assetsQuery).toEqual('my-query'); it('should load with query when searching', () => {
assetsService.setup(x => x.getAssets(app, 30, 0, 'my-query', []))
.returns(() => of(new AssetsDto(0, [])));
assetsService.verify(x => x.getAssets(app, 30, 0, 'my-query', []), Times.once()); assetsState.search('my-query').subscribe();
expect(assetsState.snapshot.assetsQuery).toEqual('my-query');
});
}); });
it('should load with tags when tag toggled', () => { describe('Updates', () => {
assetsService.setup(x => x.getAssets(app, 30, 0, undefined, ['tag1'])) beforeEach(() => {
.returns(() => of(new AssetsDto(0, []))); assetsService.setup(x => x.getAssets(app, 30, 0, undefined, []))
.returns(() => of(new AssetsDto(200, oldAssets))).verifiable();
assetsState.toggleTag('tag1').subscribe(); assetsService.setup(x => x.getTags(app))
.returns(() => of({ tag1: 1, shared: 2, tag2: 1 })).verifiable();
expect(assetsState.isTagSelected('tag1')).toBeTruthy(); assetsState.load(true).subscribe();
});
assetsService.verify(x => x.getAssets(app, 30, 0, undefined, ['tag1']), Times.once()); it('should add asset to snapshot when created', () => {
}); assetsState.add(newAsset);
it('should load with tags when tags selected', () => { expect(assetsState.snapshot.assets.values).toEqual([newAsset, ...oldAssets]);
assetsService.setup(x => x.getAssets(app, 30, 0, undefined, ['tag1', 'tag2'])) expect(assetsState.snapshot.assetsPager.numberOfItems).toBe(201);
.returns(() => of(new AssetsDto(0, []))); });
assetsState.selectTags(['tag1', 'tag2']).subscribe(); it('should increment tags when asset added', () => {
assetsState.add(newAsset);
assetsState.add(newAsset);
expect(assetsState.isTagSelected('tag1')).toBeTruthy(); expect(assetsState.snapshot.tags).toEqual({ tag1: 1, tag2: 1, shared: 2, new: 2 });
});
assetsService.verify(x => x.getAssets(app, 30, 0, undefined, ['tag1', 'tag2']), Times.once()); it('should update properties when updated', () => {
}); assetsState.update(newAsset);
const asset_1 = assetsState.snapshot.assets.at(0);
it('should load without tags when tags reset', () => { expect(asset_1).toBe(newAsset);
assetsService.setup(x => x.getAssets(app, 30, 0, undefined, [])) expect(assetsState.snapshot.tags).toEqual({ tag2: 1, shared: 1, new: 1 });
.returns(() => of(new AssetsDto(0, []))); });
assetsState.resetTags().subscribe(); it('should remove asset from snapshot when deleted', () => {
assetsService.setup(x => x.deleteAsset(app, oldAssets[0].id, version))
.returns(() => of(versioned(newVersion)));
expect(assetsState.isTagSelectionEmpty()).toBeTruthy(); assetsState.delete(oldAssets[0]).subscribe();
assetsService.verify(x => x.getAssets(app, 30, 0, undefined, []), Times.exactly(2)); expect(assetsState.snapshot.assets.values.length).toBe(1);
expect(assetsState.snapshot.assetsPager.numberOfItems).toBe(199);
expect(assetsState.snapshot.tags).toEqual({ shared: 1, tag2: 1 });
});
}); });
}); });

21
src/Squidex/app/shared/state/clients.state.spec.ts

@ -94,8 +94,23 @@ describe('ClientsState', () => {
expect(clientsState.snapshot.version).toEqual(newVersion); expect(clientsState.snapshot.version).toEqual(newVersion);
}); });
it('should update properties when updated', () => { it('should update properties when role updated', () => {
const request = { name: 'NewName', role: 'NewRole' }; const request = { role: 'Owner' };
clientsService.setup(x => x.putClient(app, oldClients[0].id, request, version))
.returns(() => of(versioned(newVersion))).verifiable();
clientsState.update(oldClients[0], request).subscribe();
const client_1 = clientsState.snapshot.clients.at(0);
expect(client_1.name).toBe('name1');
expect(client_1.role).toBe('Owner');
expect(clientsState.snapshot.version).toEqual(newVersion);
});
it('should update properties when name updated', () => {
const request = { name: 'NewName' };
clientsService.setup(x => x.putClient(app, oldClients[0].id, request, version)) clientsService.setup(x => x.putClient(app, oldClients[0].id, request, version))
.returns(() => of(versioned(newVersion))).verifiable(); .returns(() => of(versioned(newVersion))).verifiable();
@ -105,7 +120,7 @@ describe('ClientsState', () => {
const client_1 = clientsState.snapshot.clients.at(0); const client_1 = clientsState.snapshot.clients.at(0);
expect(client_1.name).toBe('NewName'); expect(client_1.name).toBe('NewName');
expect(client_1.role).toBe('NewRole'); expect(client_1.role).toBe('Developer');
expect(clientsState.snapshot.version).toEqual(newVersion); expect(clientsState.snapshot.version).toEqual(newVersion);
}); });

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

@ -14,7 +14,6 @@ import {
DialogService, DialogService,
ErrorDto, ErrorDto,
ImmutableArray, ImmutableArray,
notify,
Pager, Pager,
shareSubscribed, shareSubscribed,
State, State,
@ -312,7 +311,7 @@ export abstract class ContentsStateBase extends State<Snapshot> {
public loadVersion(content: ContentDto, version: Version): Observable<Versioned<any>> { public loadVersion(content: ContentDto, version: Version): Observable<Versioned<any>> {
return this.contentsService.getVersionData(this.appName, this.schemaName, content.id, version).pipe( return this.contentsService.getVersionData(this.appName, this.schemaName, content.id, version).pipe(
notify(this.dialogs)); shareSubscribed(this.dialogs));
} }
private get appName() { private get appName() {

12
src/Squidex/app/shared/state/contributors.state.ts

@ -132,7 +132,7 @@ export class ContributorsState extends State<Snapshot> {
const contributors = this.snapshot.contributors; const contributors = this.snapshot.contributors;
if (contributors.find(x => x.contributor.contributorId === id)) { if (contributors.find(x => x.contributor.contributorId === id)) {
return contributors.map(x => x.contributor.contributorId === id ? this.createContributor(contributor, x) : x); return contributors.map(x => x.contributor.contributorId === id ? this.createContributor(contributor) : x);
} else { } else {
return contributors.push(this.createContributor(contributor)); return contributors.push(this.createContributor(contributor));
} }
@ -161,13 +161,7 @@ export class ContributorsState extends State<Snapshot> {
return this.snapshot.version; return this.snapshot.version;
} }
private createContributor(contributor: ContributorDto, current?: SnapshotContributor): SnapshotContributor { private createContributor(contributor: ContributorDto): SnapshotContributor {
if (!contributor) { return { contributor, isCurrentUser: contributor.contributorId === this.userId };
return null!;
} else if (current && current.contributor === contributor) {
return current;
} else {
return { contributor, isCurrentUser: contributor.contributorId === this.userId };
}
} }
} }
Loading…
Cancel
Save