diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs index abfa36ac4..70faa0ef4 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs @@ -75,7 +75,7 @@ namespace Squidex.Domain.Apps.Entities.Assets } } - await UploadAsync(context, tempFile, createAsset, true, next); + await UploadAsync(context, tempFile, createAsset, createAsset.Tags, true, next); } finally { @@ -91,7 +91,7 @@ namespace Squidex.Domain.Apps.Entities.Assets try { - await UploadAsync(context, tempFile, updateAsset, false, next); + await UploadAsync(context, tempFile, updateAsset, null, false, next); } finally { @@ -107,9 +107,9 @@ namespace Squidex.Domain.Apps.Entities.Assets } } - private async Task UploadAsync(CommandContext context, string tempFile, UploadAssetCommand command, bool created, NextDelegate next) + private async Task UploadAsync(CommandContext context, string tempFile, UploadAssetCommand command, HashSet? tags, bool created, NextDelegate next) { - await EnrichWithMetadataAsync(command); + await EnrichWithMetadataAsync(command, tags); var asset = await HandleCoreAsync(context, created, next); @@ -160,7 +160,7 @@ namespace Squidex.Domain.Apps.Entities.Assets } } - private async Task EnrichWithMetadataAsync(UploadAssetCommand command, HashSet? tags = null) + private async Task EnrichWithMetadataAsync(UploadAssetCommand command, HashSet? tags) { foreach (var metadataSource in assetMetadataSources) { diff --git a/frontend/app/framework/angular/forms/progress-bar.component.ts b/frontend/app/framework/angular/forms/progress-bar.component.ts index 9bf8d0aea..6edd18973 100644 --- a/frontend/app/framework/angular/forms/progress-bar.component.ts +++ b/frontend/app/framework/angular/forms/progress-bar.component.ts @@ -11,6 +11,11 @@ import * as ProgressBar from 'progressbar.js'; @Component({ selector: 'sqx-progress-bar', + styles: [` + :host /deep/ svg { + vertical-align: top + }` + ], template: '', changeDetection: ChangeDetectionStrategy.OnPush }) diff --git a/frontend/app/framework/angular/http/http-extensions.ts b/frontend/app/framework/angular/http/http-extensions.ts index 25caa0645..3b19d5b86 100644 --- a/frontend/app/framework/angular/http/http-extensions.ts +++ b/frontend/app/framework/angular/http/http-extensions.ts @@ -17,7 +17,7 @@ import { } from '@app/framework/internal'; export module HTTP { - export function upload(http: HttpClient, method: string, url: string, file: File, version?: Version): Observable> { + export function upload(http: HttpClient, method: string, url: string, file: Blob, version?: Version): Observable> { const req = new HttpRequest(method, url, getFormData(file), { headers: createHeaders(version), reportProgress: true }); return http.request(req); @@ -59,7 +59,7 @@ export module HTTP { return handleVersion(http.request(method, url, { observe: 'response', headers, body })); } - function getFormData(file: File) { + function getFormData(file: Blob) { const formData = new FormData(); formData.append('file', file); diff --git a/frontend/app/shared/components/assets/asset-dialog.component.html b/frontend/app/shared/components/assets/asset-dialog.component.html index 8b3a41d1a..cc454808f 100644 --- a/frontend/app/shared/components/assets/asset-dialog.component.html +++ b/frontend/app/shared/components/assets/asset-dialog.component.html @@ -11,7 +11,14 @@ - + + + + + + + + @@ -19,6 +26,15 @@
+ +
+ + +
diff --git a/frontend/app/shared/components/assets/asset-dialog.component.scss b/frontend/app/shared/components/assets/asset-dialog.component.scss index 61f848c07..982f27139 100644 --- a/frontend/app/shared/components/assets/asset-dialog.component.scss +++ b/frontend/app/shared/components/assets/asset-dialog.component.scss @@ -3,7 +3,17 @@ } .image { - @include absolute(0, 0, 0, 0); + & { + @include absolute(0, 0, 0, 0); + } + + &-progress { + @include absolute(0, 0, null, 0); + } +} + +.invisible { + visibility: hidden; } .metadata { diff --git a/frontend/app/shared/components/assets/asset-dialog.component.ts b/frontend/app/shared/components/assets/asset-dialog.component.ts index c19da7262..a6f4d5125 100644 --- a/frontend/app/shared/components/assets/asset-dialog.component.ts +++ b/frontend/app/shared/components/assets/asset-dialog.component.ts @@ -5,15 +5,21 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, QueryList, ViewChildren } from '@angular/core'; import { FormBuilder } from '@angular/forms'; import { AnnotateAssetForm, AssetDto, - AssetsState + AssetsState, + AssetUploaderState, + DialogService, + Types, + UploadCanceled } from '@app/shared/internal'; +import { ImageEditorComponent } from './image-editor.component'; + @Component({ selector: 'sqx-asset-dialog', styleUrls: ['./asset-dialog.component.scss'], @@ -24,30 +30,52 @@ export class AssetDialogComponent implements OnInit { @Output() public complete = new EventEmitter(); + @Output() + public changed = new EventEmitter(); + @Input() public asset: AssetDto; @Input() public allTags: ReadonlyArray; + @ViewChildren(ImageEditorComponent) + public imageEditor: QueryList; + public isEditable = false; + public isEditableAny = false; + public isUploadable = false; + + public progress = 0; - public selectableTabs: ReadonlyArray = ['Image', 'Metadata']; - public selectedTab = this.selectableTabs[0]; + public selectableTabs: ReadonlyArray; + public selectedTab: string; public annotateForm = new AnnotateAssetForm(this.formBuilder); constructor( private readonly assetsState: AssetsState, + private readonly assetUploader: AssetUploaderState, + private readonly changeDetector: ChangeDetectorRef, + private readonly dialogs: DialogService, private readonly formBuilder: FormBuilder ) { } public ngOnInit() { this.isEditable = this.asset.canUpdate; + this.isUploadable = this.asset.canUpload; this.annotateForm.load(this.asset); this.annotateForm.setEnabled(this.isEditable); + + if (this.asset.type === 'Image') { + this.selectableTabs = ['Image', 'Metadata']; + } else { + this.selectableTabs = ['Metadata']; + } + + this.selectTab(this.selectableTabs[0]); } public selectTab(tab: string) { @@ -63,21 +91,58 @@ export class AssetDialogComponent implements OnInit { } public annotateAsset() { - if (!this.isEditable) { - return; + if (this.selectedTab === 'Image') { + if (!this.isUploadable) { + return; + } + + const file = this.imageEditor.first.toFile(); + + if (file) { + this.setProgress(0); + + this.assetUploader.uploadAsset(this.asset, file) + .subscribe(dto => { + if (Types.isNumber(dto)) { + this.setProgress(dto); + } else { + this.changed.emit(dto); + } + + this.dialogs.notifyInfo('Asset has been updated.'); + }, error => { + if (!Types.is(error, UploadCanceled)) { + this.dialogs.notifyError(error); + } + }, () => { + this.setProgress(0); + }); + } else { + this.dialogs.notifyInfo('Nothing has changed.'); + } + } else { + if (!this.isEditable) { + return; + } + + const value = this.annotateForm.submit(this.asset); + + if (value) { + this.assetsState.updateAsset(this.asset, value) + .subscribe(() => { + this.annotateForm.submitCompleted({ noReset: true }); + + this.dialogs.notifyInfo('Asset has been updated.'); + }, error => { + this.annotateForm.submitFailed(error); + }); + } } + } - const value = this.annotateForm.submit(this.asset); - - if (value) { - this.assetsState.updateAsset(this.asset, value) - .subscribe(() => { - this.emitComplete(); - }, error => { - this.annotateForm.submitFailed(error); - }); - } else if (this.annotateForm.form.valid) { - this.emitComplete(); - } + public setProgress(progress: number) { + this.progress = progress; + + this.changeDetector.markForCheck(); } } \ No newline at end of file diff --git a/frontend/app/shared/components/assets/asset.component.html b/frontend/app/shared/components/assets/asset.component.html index bcdd6cf2f..a4cd1784c 100644 --- a/frontend/app/shared/components/assets/asset.component.html +++ b/frontend/app/shared/components/assets/asset.component.html @@ -153,6 +153,7 @@ diff --git a/frontend/app/shared/components/assets/asset.component.ts b/frontend/app/shared/components/assets/asset.component.ts index 6e17c0f9a..b06555719 100644 --- a/frontend/app/shared/components/assets/asset.component.ts +++ b/frontend/app/shared/components/assets/asset.component.ts @@ -115,8 +115,7 @@ export class AssetComponent implements OnInit { this.setProgress(asset); } else { this.setProgress(0); - - this.asset = asset; + this.setAsset(asset); } }, error => { this.dialogs.notifyError(error); @@ -142,7 +141,13 @@ export class AssetComponent implements OnInit { this.loadError.emit(error); } - private setProgress(progress: number) { + public setAsset(asset: AssetDto) { + this.asset = asset; + + this.changeDetector.markForCheck(); + } + + public setProgress(progress: number) { this.progress = progress; this.changeDetector.markForCheck(); diff --git a/frontend/app/shared/components/assets/image-editor.component.ts b/frontend/app/shared/components/assets/image-editor.component.ts index 8bce3198b..aa0d96daa 100644 --- a/frontend/app/shared/components/assets/image-editor.component.ts +++ b/frontend/app/shared/components/assets/image-editor.component.ts @@ -102,6 +102,8 @@ const blackTheme = { }) export class ImageEditorComponent implements AfterViewInit, OnChanges { private imageEditor: any; + private isChanged = false; + private isChangedBefore = false; @Input() public imageUrl: string; @@ -120,6 +122,29 @@ export class ImageEditorComponent implements AfterViewInit, OnChanges { } } + public toFile(): Blob | null { + if (!this.isChanged) { + return null; + } + + this.isChanged = false; + + const dataURI = this.imageEditor.toDataURL(); + + const byteString = atob(dataURI.split(',')[1]); + const byteBuffer = new ArrayBuffer(byteString.length); + + const type = dataURI.split(',')[0].split(':')[1].split(';')[0]; + + const array = new Uint8Array(byteBuffer); + + for (let i = 0; i < byteString.length; i++) { + array[i] = byteString.charCodeAt(i); + } + + return new Blob([array], { type }); + } + public ngAfterViewInit() { const styles = [ 'https://uicdn.toast.com/tui-color-picker/latest/tui-color-picker.css', @@ -154,6 +179,14 @@ export class ImageEditorComponent implements AfterViewInit, OnChanges { theme: blackTheme } }); + + this.imageEditor.on('undoStackChanged', () => { + if (this.isChangedBefore) { + this.isChanged = true; + } else { + this.isChangedBefore = true; + } + }); }); } } \ No newline at end of file diff --git a/frontend/app/shared/services/assets.service.ts b/frontend/app/shared/services/assets.service.ts index 401888279..d82bf017e 100644 --- a/frontend/app/shared/services/assets.service.ts +++ b/frontend/app/shared/services/assets.service.ts @@ -233,7 +233,7 @@ export class AssetsService { pretifyError('Failed to load assets. Please reload.')); } - public postAssetFile(appName: string, file: File, parentId?: string): Observable { + public postAssetFile(appName: string, file: Blob, parentId?: string): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets?parentId=${parentId}`); return HTTP.upload(this.http, 'POST', url, file).pipe( @@ -266,7 +266,7 @@ export class AssetsService { pretifyError('Failed to upload asset. Please reload.')); } - public putAssetFile(appName: string, resource: Resource, file: File, version: Version): Observable { + public putAssetFile(appName: string, resource: Resource, file: Blob, version: Version): Observable { const link = resource._links['upload']; const url = this.apiUrl.buildUrl(link.href); diff --git a/frontend/app/shared/state/asset-uploader.state.ts b/frontend/app/shared/state/asset-uploader.state.ts index b1db14991..afdf551a2 100644 --- a/frontend/app/shared/state/asset-uploader.state.ts +++ b/frontend/app/shared/state/asset-uploader.state.ts @@ -75,9 +75,9 @@ export class AssetUploaderState extends State { const stream = this.assetsService.postAssetFile(this.appName, file, parentId); - return this.upload(stream, MathHelper.guid(), file, asset => { + return this.upload(stream, MathHelper.guid(), file.name, asset => { if (asset.isDuplicate) { - this.dialogs.notifyError('Asset has already been uploaded.'); + this.dialogs.notifyInfo('Asset has already been uploaded.'); } else if (target) { target.addAsset(asset); } @@ -86,14 +86,14 @@ export class AssetUploaderState extends State { }); } - public uploadAsset(asset: AssetDto, file: File): Observable { + public uploadAsset(asset: AssetDto, file: Blob): Observable { const stream = this.assetsService.putAssetFile(this.appName, asset, file, asset.version); - return this.upload(stream, asset.id, file); + return this.upload(stream, asset.id, file['name'] || asset.fileName); } - private upload(source: Observable, id: string, file: File, complete?: ((completion: AssetDto) => AssetDto)) { - let upload = { id, name: file.name, progress: 1, status: 'Running', cancel: new Subject() }; + private upload(source: Observable, id: string, name: string, complete?: ((completion: AssetDto) => AssetDto)) { + let upload = { id, name, progress: 1, status: 'Running', cancel: new Subject() }; this.addUpload(upload); diff --git a/frontend/app/shared/state/assets.forms.ts b/frontend/app/shared/state/assets.forms.ts index 90049637e..160879f8e 100644 --- a/frontend/app/shared/state/assets.forms.ts +++ b/frontend/app/shared/state/assets.forms.ts @@ -42,11 +42,7 @@ export class AnnotateAssetForm extends Form