Browse Source

T

pull/356/head
Sebastian Stehle 7 years ago
parent
commit
d24d4961bd
  1. 20
      src/Squidex/app/shared/components/asset.component.html
  2. 91
      src/Squidex/app/shared/components/asset.component.ts
  3. 14
      src/Squidex/app/shared/state/assets.state.spec.ts
  4. 194
      src/Squidex/app/shared/state/assets.state.ts

20
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)="emitSelect()" (sqxFileDrop)="updateFile($event)" [noDrop]="true">
<div class="card-body">
<div class="file-preview" *ngIf="asset && !upload" @fade>
<div class="file-preview" *ngIf="asset && snapshot.progress === 0" @fade>
<span class="file-type" *ngIf="asset.fileType">
{{asset.fileType}}
</span>
@ -46,11 +46,11 @@
</div>
</div>
<div class="upload-progress" *ngIf="upload">
<sqx-progress-bar mode="Circle" [value]="upload.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 && !upload">
<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>
@ -77,14 +77,14 @@
<div class="table-items-row" [class.selectable]="isSelectable" (click)="emitSelect()" (sqxFileDrop)="updateFile($event)" [noDrop]="true">
<div class="left-border" [class.hidden]="!isSelectable" [class.selected]="isSelected" ></div>
<div *ngIf="asset && asset.canPreview && !upload" 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 && !upload" 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>
<table class="table-fixed" *ngIf="asset && !upload" @fade>
<table class="table-fixed" *ngIf="asset && snapshot.progress === 0" @fade>
<tr>
<td class="col-name">
<div class="file-name editable" (click)="edit()">
@ -113,11 +113,11 @@
</tr>
</table>
<div class="upload-progress" *ngIf="upload">
<sqx-progress-bar [value]="upload.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 && !upload">
<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>

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

@ -5,15 +5,26 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, EventEmitter, HostBinding, Input, Output } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, HostBinding, Input, OnInit, Output } from '@angular/core';
import {
AppsState,
AssetDto,
AssetsService,
AuthService,
DateTime,
DialogModel,
DialogService,
fadeAnimation,
UploadingAsset
StatefulComponent,
Types,
Versioned
} from '@app/shared/internal';
interface State {
progress: number;
}
@Component({
selector: 'sqx-asset',
styleUrls: ['./asset.component.scss'],
@ -23,9 +34,9 @@ import {
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AssetComponent {
export class AssetComponent extends StatefulComponent<State> implements OnInit {
@Input()
public upload: UploadingAsset;
public initFile: File;
@Input()
public asset: AssetDto;
@ -52,13 +63,16 @@ export class AssetComponent {
public allTags: string[];
@Output()
public remove = new EventEmitter<AssetDto>();
public load = new EventEmitter<AssetDto>();
@Output()
public update = new EventEmitter<AssetDto>();
public loadError = new EventEmitter();
@Output()
public uploadFile = new EventEmitter<File>();
public remove = new EventEmitter<AssetDto>();
@Output()
public update = new EventEmitter<AssetDto>();
@Output()
public delete = new EventEmitter<AssetDto>();
@ -68,9 +82,54 @@ export class AssetComponent {
public editDialog = new DialogModel();
public updateFile(files: File[]) {
constructor(changeDetector: ChangeDetectorRef,
private readonly appsState: AppsState,
private readonly assetsService: AssetsService,
private readonly authState: AuthService,
private readonly dialogs: DialogService
) {
super(changeDetector, {
progress: 0
});
}
public ngOnInit() {
const initFile = this.initFile;
if (initFile) {
this.setProgress(1);
this.assetsService.uploadFile(this.appsState.appName, initFile, this.authState.user!.token, DateTime.now())
.subscribe(dto => {
if (Types.is(dto, AssetDto)) {
this.emitLoad(dto);
} else {
this.setProgress(dto);
}
}, error => {
this.dialogs.notifyError(error);
this.emitLoadError(error);
});
}
}
public updateFile(files: FileList) {
if (files.length === 1) {
this.uploadFile.emit(files[0]);
this.setProgress(1);
this.assetsService.replaceFile(this.appsState.appName, this.asset.id, files[0], this.asset.version)
.subscribe(dto => {
if (Types.is(dto, Versioned)) {
this.updateAsset(this.asset.update(dto.payload, this.authState.user!.token, dto.version), true);
} else {
this.setProgress(dto);
}
}, error => {
this.dialogs.notifyError(error);
this.setProgress(0);
});
}
}
@ -92,6 +151,14 @@ export class AssetComponent {
this.delete.emit(this.asset);
}
public emitLoad(asset: AssetDto) {
this.load.emit(asset);
}
public emitLoadError(error: any) {
this.loadError.emit(error);
}
public emitUpdate() {
this.update.emit(this.asset);
}
@ -100,6 +167,10 @@ export class AssetComponent {
this.remove.emit(this.asset);
}
private setProgress(progress: number) {
this.next(s => ({ ...s, progress }));
}
public updateAsset(asset: AssetDto, emitEvent: boolean) {
this.asset = asset;
@ -107,6 +178,8 @@ export class AssetComponent {
this.emitUpdate();
}
this.next(s => ({ ...s, progress: 0 }));
this.cancelEdit();
}
}

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

@ -23,7 +23,6 @@ describe('AssetsState', () => {
const {
app,
appsState,
authService,
creation,
creator,
modified,
@ -52,7 +51,7 @@ describe('AssetsState', () => {
assetsService.setup(x => x.getTags(app))
.returns(() => of({ tag1: 1, shared: 2, tag2: 1 }));
assetsState = new AssetsState(appsState.object, assetsService.object, authService.object, dialogs.object);
assetsState = new AssetsState(appsState.object, assetsService.object, dialogs.object);
assetsState.load().subscribe();
});
@ -77,6 +76,15 @@ describe('AssetsState', () => {
dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once());
});
it('should add asset to snapshot when created', () => {
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);
expect(assetsState.snapshot.assets.values).toEqual([newAsset, ...oldAssets]);
expect(assetsState.snapshot.assetsPager.numberOfItems).toBe(201);
});
it('should update properties when updated', () => {
const newAsset = new AssetDto('id1', modifier, modifier, modified, modified, 'name3', 'hash3', 'type3', 3, 3, 'mime3', false, true, 0, 0, 'slug3', ['new'], 'url3', version);
@ -90,7 +98,7 @@ describe('AssetsState', () => {
it('should remove asset from snapshot when deleted', () => {
assetsService.setup(x => x.deleteAsset(app, oldAssets[0].id, version))
.returns(() => of(new Versioned(newVersion, {})));
.returns(() => of(new Versioned<any>(newVersion, {})));
assetsState.delete(oldAssets[0]).subscribe();

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

@ -6,49 +6,20 @@
*/
import { Injectable } from '@angular/core';
import { combineLatest, Observable, Subject, Subscription } from 'rxjs';
import { combineLatest, Observable } from 'rxjs';
import { distinctUntilChanged, map, tap } from 'rxjs/operators';
import {
DateTime,
DialogService,
ImmutableArray,
MathHelper,
notify,
Pager,
State,
Types
State
} from '@app/framework';
import { AuthService } from '../services/auth.service';
import { AssetDto, AssetsService } from './../services/assets.service';
import { AssetDto, AssetsService} from './../services/assets.service';
import { AppsState } from './apps.state';
export interface UploadingAsset {
// Unique id.
id: string;
// The name of the asset.
name: string;
// The upload subscription.
subscription: Subscription;
// The progress.
progress: number;
// Indicates if the upload has been completed.
isCompleted?: boolean;
}
export interface AssetWithUpload {
// The asset.
asset: AssetDto;
// The corresponding upload.
upload?: UploadingAsset;
}
interface Snapshot {
// All assets tags.
tags: { [name: string]: number };
@ -56,12 +27,6 @@ interface Snapshot {
// The selected asset tags.
tagsSelected: { [name: string]: boolean };
// The uploads.
uploadsDirect: ImmutableArray<UploadingAsset>;
// The uploads removed with a delay.
uploadsDelayed: ImmutableArray<UploadingAsset>;
// The current assets.
assets: ImmutableArray<AssetDto>;
@ -93,10 +58,6 @@ export class AssetsState extends State<Snapshot> {
this.changes.pipe(map(x => x.assets),
distinctUntilChanged());
public assetsWithUploads =
this.changes.pipe(map(x => getAssetsWithUploads(x)),
distinctUntilChanged());
public assetsQuery =
this.changes.pipe(map(x => x.assetsQuery),
distinctUntilChanged());
@ -105,14 +66,6 @@ export class AssetsState extends State<Snapshot> {
this.changes.pipe(map(x => x.assetsPager),
distinctUntilChanged());
public uploads =
this.changes.pipe(map(x => x.uploadsDirect),
distinctUntilChanged());
public uploadsDelayed =
this.changes.pipe(map(x => x.uploadsDelayed),
distinctUntilChanged());
public isLoaded =
this.changes.pipe(map(x => !!x.isLoaded),
distinctUntilChanged());
@ -120,17 +73,9 @@ export class AssetsState extends State<Snapshot> {
constructor(
private readonly appsState: AppsState,
private readonly assetsService: AssetsService,
private readonly authService: AuthService,
private readonly dialogs: DialogService
) {
super({
assets: ImmutableArray.empty(),
assetsPager: new Pager(0, 0, 30),
uploadsDirect: ImmutableArray.empty(),
uploadsDelayed: ImmutableArray.empty(),
tags: {},
tagsSelected: {}
});
super({ assets: ImmutableArray.empty(), assetsPager: new Pager(0, 0, 30), tags: {}, tagsSelected: {} });
}
public load(isReload = false): Observable<any> {
@ -165,100 +110,6 @@ export class AssetsState extends State<Snapshot> {
notify(this.dialogs));
}
public remove(upload: UploadingAsset, delayed = false) {
upload.subscription.unsubscribe();
this.next(s => {
let uploadsDirect = s.uploadsDirect.removeBy('id', upload);
let uploadsDelayed = s.uploadsDelayed;
if (!delayed) {
uploadsDelayed = s.uploadsDelayed.removeBy('id', upload);
}
return { ...s, uploadsDirect, uploadsDelayed };
});
if (!delayed) {
setTimeout(() => {
this.remove(upload, true);
}, 10000);
}
}
public upload(file: File, now?: DateTime): Observable<number | AssetDto> {
const observable = this.assetsService.uploadFile(this.appName, file, this.user, now || DateTime.now());
let upload: UploadingAsset;
const subject = new Subject<number | AssetDto>();
const subscription = observable.subscribe(event => {
if (Types.isNumber(event)) {
this.setProgress(upload, event);
subject.next(event);
} else {
if (event.isDuplicate) {
this.dialogs.notifyError('The same asset has already been uploaded.');
} else {
this.add(event);
}
subject.next(event);
}
}, error => {
subject.error(error);
this.remove(upload, true);
}, () => {
subject.complete();
this.remove(upload, true);
});
upload = { id: MathHelper.guid(), name: file.name, progress: 0, subscription };
this.addUpload(upload);
return subject;
}
public replaceFile(asset: AssetDto, file: File, now?: DateTime): Observable<number | AssetDto> {
const observable = this.assetsService.replaceFile(this.appName, asset.id, file, asset.version);
let upload: UploadingAsset;
const subject = new Subject<number | AssetDto>();
const subscription = observable.subscribe(event => {
if (Types.isNumber(event)) {
this.setProgress(upload, event);
subject.next(event);
} else {
const newAsset = asset.update(event.payload, this.user, event.version, now || DateTime.now());
this.update(newAsset);
subject.next(newAsset);
}
}, error => {
subject.error(error);
this.remove(upload, true);
}, () => {
subject.complete();
this.remove(upload, true);
});
upload = { id: asset.id, name: file.name, progress: 0, subscription };
this.addUpload(upload);
return subject;
}
public add(asset: AssetDto) {
this.next(s => {
const assets = s.assets.pushFront(asset);
@ -373,41 +224,9 @@ export class AssetsState extends State<Snapshot> {
return Object.keys(this.snapshot.tagsSelected).length === 0;
}
private setProgress(upload: UploadingAsset, progress: number) {
this.next(s => {
const newUpload = { ...upload, progress };
const uploadsDirect = s.uploadsDirect.replaceBy('id', newUpload);
const uploadsDelayed = s.uploadsDelayed.replaceBy('id', newUpload);
return { ...s, uploadsDirect, uploadsDelayed };
});
}
private addUpload(upload: UploadingAsset) {
this.next(s => {
const uploadsDirect = s.uploadsDirect.pushFront(upload);
const uploadsDelayed = s.uploadsDelayed.pushFront(upload);
return { ...s, uploadsDirect, uploadsDelayed };
});
}
private get appName() {
return this.appsState.appName;
}
private get user() {
return this.authService.user!.token;
}
}
function getAssetsWithUploads(state: Snapshot) {
return state.assets.map(asset => {
const upload = state.uploadsDirect.find(x => x.id === x.id);
return { upload, asset };
});
}
function addTags(asset: AssetDto, tags: { [x: string]: number; }) {
@ -443,4 +262,7 @@ function sort(tags: { [name: string]: number }) {
}).map(key => {
return { name: key, count: tags[key] };
});
}
}
@Injectable()
export class AssetsDialogState extends AssetsState { }
Loading…
Cancel
Save