From 174d56371484077f05f9b07a9e9d28ba7ad0dbee Mon Sep 17 00:00:00 2001 From: erickgonzalez Date: Mon, 22 May 2023 14:57:41 -0600 Subject: [PATCH] #23863 include in 23.01.3 --- .../dot-block-editor.component.scss | 17 ++ .../dot-block-editor.component.stories.ts | 4 +- .../asset-form/asset-form.component.html | 23 ++- .../asset-form/asset-form.component.scss | 6 +- .../asset-form/asset-form.component.ts | 9 ++ .../asset-form/asset-form.extension.ts | 17 ++ .../dot-upload-asset/animations/index.ts | 33 ++++ .../dot-asset-preview.component.html | 2 +- .../dot-upload-asset.component.html | 32 ++-- .../dot-upload-asset.component.scss | 27 +++- .../dot-upload-asset.component.ts | 152 +++++++++++++++--- .../image-uploader.extension.ts | 2 +- .../services/dot-image/dot-image.service.ts | 30 ++-- .../src/lib/nodes/image-node/image.node.ts | 2 +- .../src/lib/nodes/video/video.node.ts | 31 +++- core-web/libs/data-access/src/index.ts | 1 + .../dot-upload}/dot-upload.service.spec.ts | 7 +- .../src/lib/dot-upload/dot-upload.service.ts | 137 ++++++++++++++++ .../src/components/dot-form/dot-form.tsx | 7 +- .../dot-form/services/dot-upload.service.ts | 67 -------- core-web/libs/utils/src/index.ts | 1 - .../src/lib/services/dot-asset.service.ts | 6 +- .../src/lib/services/dot-temp-file.service.ts | 141 ---------------- 23 files changed, 467 insertions(+), 287 deletions(-) create mode 100644 core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-upload-asset/animations/index.ts create mode 100644 core-web/libs/data-access/src/index.ts rename core-web/libs/{dotcms-field-elements/src/components/dot-form/services => data-access/src/lib/dot-upload}/dot-upload.service.spec.ts (83%) create mode 100644 core-web/libs/data-access/src/lib/dot-upload/dot-upload.service.ts delete mode 100644 core-web/libs/dotcms-field-elements/src/components/dot-form/services/dot-upload.service.ts delete mode 100644 core-web/libs/utils/src/lib/services/dot-temp-file.service.ts diff --git a/core-web/libs/block-editor/src/lib/components/dot-block-editor/dot-block-editor.component.scss b/core-web/libs/block-editor/src/lib/components/dot-block-editor/dot-block-editor.component.scss index 344d2cecd726..8063fe09fafe 100644 --- a/core-web/libs/block-editor/src/lib/components/dot-block-editor/dot-block-editor.component.scss +++ b/core-web/libs/block-editor/src/lib/components/dot-block-editor/dot-block-editor.component.scss @@ -256,6 +256,23 @@ } } + video { + max-width: 100%; + max-height: 100%; + height: auto; + } + + .vertical-video { + max-height: 400px; + width: auto; + } + + .horizontal-video, + .node-image { + height: auto; + width: 50%; + } + .node-container { margin-bottom: $spacing-3; } diff --git a/core-web/libs/block-editor/src/lib/components/dot-block-editor/dot-block-editor.component.stories.ts b/core-web/libs/block-editor/src/lib/components/dot-block-editor/dot-block-editor.component.stories.ts index bf1c8a00b669..b044e30a7df0 100644 --- a/core-web/libs/block-editor/src/lib/components/dot-block-editor/dot-block-editor.component.stories.ts +++ b/core-web/libs/block-editor/src/lib/components/dot-block-editor/dot-block-editor.component.stories.ts @@ -1,5 +1,6 @@ import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { of } from 'rxjs'; @@ -43,6 +44,7 @@ export const primary = () => ({ BlockEditorModule, OrderListModule, ListboxModule, + BrowserModule, BrowserAnimationsModule ], providers: [ @@ -88,7 +90,7 @@ export const primary = () => ({ } } ]).pipe( - delay(1500), + delay(5000000), tap(() => statusCallback(FileStatus.IMPORT)) ); } diff --git a/core-web/libs/block-editor/src/lib/extensions/asset-form/asset-form.component.html b/core-web/libs/block-editor/src/lib/extensions/asset-form/asset-form.component.html index 51af1a5ca44a..54431d685855 100644 --- a/core-web/libs/block-editor/src/lib/extensions/asset-form/asset-form.component.html +++ b/core-web/libs/block-editor/src/lib/extensions/asset-form/asset-form.component.html @@ -1,6 +1,11 @@
- +
- +
- +
void; + @Input() preventClose: (value: boolean) => void; + @Input() onHide: (value: boolean) => void; + + public disableTabs = false; + + public onPreventClose(value) { + this.preventClose(value); + this.disableTabs = value; + } } diff --git a/core-web/libs/block-editor/src/lib/extensions/asset-form/asset-form.extension.ts b/core-web/libs/block-editor/src/lib/extensions/asset-form/asset-form.extension.ts index a120a3f57082..afef05b0f785 100644 --- a/core-web/libs/block-editor/src/lib/extensions/asset-form/asset-form.extension.ts +++ b/core-web/libs/block-editor/src/lib/extensions/asset-form/asset-form.extension.ts @@ -55,6 +55,7 @@ export const BubbleAssetFormExtension = (viewContainerRef: ViewContainerRef) => let formTippy: Instance | undefined; let component: ComponentRef; let element: Element; + let preventClose = false; function onStart({ editor, type, getPosition }: StartProps) { setUpTippy(editor); @@ -69,6 +70,10 @@ export const BubbleAssetFormExtension = (viewContainerRef: ViewContainerRef) => } function onHide(editor): void { + if (preventClose) { + return; + } + editor.commands.closeAssetForm(); formTippy?.hide(); component?.destroy(); @@ -95,13 +100,25 @@ export const BubbleAssetFormExtension = (viewContainerRef: ViewContainerRef) => component.instance.languageId = editor.storage.dotConfig.lang; component.instance.type = type; component.instance.onSelectAsset = (payload) => { + onPreventClose(editor, false); editor.chain().insertAsset({ type, payload }).addNextLine().closeAssetForm().run(); }; + component.instance.preventClose = (value) => onPreventClose(editor, value); + component.instance.onHide = () => { + onPreventClose(editor, false); + onHide(editor); + }; + element = component.location.nativeElement; component.changeDetectorRef.detectChanges(); } + function onPreventClose(editor, value) { + preventClose = value; + editor.setOptions({ editable: !value }); + } + return BubbleMenu.extend({ name: 'bubbleAssetForm', diff --git a/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-upload-asset/animations/index.ts b/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-upload-asset/animations/index.ts new file mode 100644 index 000000000000..0d589b133c91 --- /dev/null +++ b/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-upload-asset/animations/index.ts @@ -0,0 +1,33 @@ +import { animate, keyframes, state, style, transition, trigger } from '@angular/animations'; + +export const shakeAnimation = trigger('shakeit', [ + state( + 'shakestart', + style({ + transform: 'scale(1.1)' + }) + ), + state( + 'shakeend', + style({ + transform: 'scale(1)' + }) + ), + transition( + 'shakestart => shakeend', + animate( + '1000ms ease-in', + keyframes([ + style({ transform: 'translate3d(-2px, 0, 0)', offset: 0.1 }), + style({ transform: 'translate3d(4px, 0, 0)', offset: 0.2 }), + style({ transform: 'translate3d(-4px, 0, 0)', offset: 0.3 }), + style({ transform: 'translate3d(4px, 0, 0)', offset: 0.4 }), + style({ transform: 'translate3d(-4px, 0, 0)', offset: 0.5 }), + style({ transform: 'translate3d(4px, 0, 0)', offset: 0.6 }), + style({ transform: 'translate3d(-4px, 0, 0)', offset: 0.7 }), + style({ transform: 'translate3d(4px, 0, 0)', offset: 0.8 }), + style({ transform: 'translate3d(-2px, 0, 0)', offset: 0.9 }) + ]) + ) + ) +]); diff --git a/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-upload-asset/components/dot-asset-preview/dot-asset-preview.component.html b/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-upload-asset/components/dot-asset-preview/dot-asset-preview.component.html index 015c17ff871c..a1b0701e050e 100644 --- a/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-upload-asset/components/dot-asset-preview/dot-asset-preview.component.html +++ b/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-upload-asset/components/dot-asset-preview/dot-asset-preview.component.html @@ -4,7 +4,7 @@

Select an accepted asset type

diff --git a/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-upload-asset/dot-upload-asset.component.html b/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-upload-asset/dot-upload-asset.component.html index b9896ace3894..f1d96b77a811 100644 --- a/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-upload-asset/dot-upload-asset.component.html +++ b/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-upload-asset/dot-upload-asset.component.html @@ -11,33 +11,25 @@
- - + + {{ error }}
-
- -
diff --git a/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-upload-asset/dot-upload-asset.component.scss b/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-upload-asset/dot-upload-asset.component.scss index a072715979a3..6cce137528bc 100644 --- a/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-upload-asset/dot-upload-asset.component.scss +++ b/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-upload-asset/dot-upload-asset.component.scss @@ -10,6 +10,13 @@ align-items: center; flex-direction: column; + .error { + color: $error; + font-size: $font-size-large; + max-width: 100%; + white-space: pre-wrap; + } + .preview-container { align-items: center; display: flex; @@ -17,6 +24,7 @@ justify-content: center; overflow: hidden; width: 100%; + flex-direction: column; img { height: 100%; @@ -25,11 +33,24 @@ } } - .btn-container { + .action-container { display: flex; - gap: $spacing-3; height: fit-content; - justify-content: end; + justify-content: space-between; width: 100%; } + + .loading-message { + display: flex; + justify-content: center; + align-items: center; + gap: $spacing-3; + } + + .warning { + display: block; + font-style: normal; + text-align: center; + color: $black; + } } diff --git a/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-upload-asset/dot-upload-asset.component.ts b/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-upload-asset/dot-upload-asset.component.ts index 767ec24fb17d..7958537a1b24 100644 --- a/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-upload-asset/dot-upload-asset.component.ts +++ b/core-web/libs/block-editor/src/lib/extensions/asset-form/components/dot-upload-asset/dot-upload-asset.component.ts @@ -1,45 +1,86 @@ +import { Subscription, throwError } from 'rxjs'; + +import { HttpErrorResponse } from '@angular/common/http'; + import { Component, EventEmitter, Output, Input, ChangeDetectorRef, - ChangeDetectionStrategy + ChangeDetectionStrategy, + OnDestroy, + HostListener, + ElementRef } from '@angular/core'; -import { DomSanitizer } from '@angular/platform-browser'; +import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; + +import { catchError, take } from 'rxjs/operators'; import { DotCMSContentlet, EditorAssetTypes } from '@dotcms/dotcms-models'; +import { shakeAnimation } from './animations'; + import { DotImageService } from '../../../image-uploader/services/dot-image/dot-image.service'; export enum STATUS { SELECT = 'SELECT', PREVIEW = 'PREVIEW', - UPLOAD = 'UPLOAD' + UPLOAD = 'UPLOAD', + ERROR = 'ERROR' } @Component({ selector: 'dot-upload-asset', templateUrl: './dot-upload-asset.component.html', styleUrls: ['./dot-upload-asset.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, + animations: [shakeAnimation] }) -export class DotUploadAssetComponent { +export class DotUploadAssetComponent implements OnDestroy { @Output() uploadedFile = new EventEmitter(); + @Output() + preventClose = new EventEmitter(); + + @Output() + hide = new EventEmitter(); + @Input() type: EditorAssetTypes; public status = STATUS.SELECT; public file: File; - public src: string | ArrayBuffer; + public src: string | ArrayBuffer | SafeResourceUrl; + public error: string; + public animation = 'shakeend'; + public $uploadRequestSubs: Subscription; + public controller: AbortController; + + get errorMessage() { + return ` Don't close this window while the ${this.type} uploads`; + } + + @HostListener('window:click', ['$event.target']) onClick(e) { + const clickedOutside = !this.el.nativeElement.contains(e); + + // If it's uploading and the user click outside the component, shake the message + if (this.status === STATUS.UPLOAD && clickedOutside) { + this.shakeMe(); + } + } constructor( private readonly sanitizer: DomSanitizer, private readonly imageService: DotImageService, - private readonly cd: ChangeDetectorRef + private readonly cd: ChangeDetectorRef, + private readonly el: ElementRef ) {} + ngOnDestroy(): void { + this.preventClose.emit(false); + } + /** * Set Selected File * @@ -49,9 +90,14 @@ export class DotUploadAssetComponent { onSelectFile(files: File[]) { const file = files[0]; const reader = new FileReader(); + this.preventClose.emit(true); reader.onload = (e) => this.setFile(file, e.target.result); - // Allows us to get a secure url without usin the Angular bypasssecuritytrusthtml - reader.readAsDataURL(file); + /* + * We can not use `reader.readDataAsUrl()` method because of this: + * https://stackoverflow.com/questions/40325410/filereader-is-unable-to-read-large-files + * + */ + reader.readAsArrayBuffer(file); } /** @@ -59,9 +105,39 @@ export class DotUploadAssetComponent { * * @memberof DotUploadAssetComponent */ - removeFile() { + cancelAction() { this.file = null; this.status = STATUS.SELECT; + this.cancelUploading(); + this.hide.emit(true); + + return; + + // this.preventClose.emit(false); + } + + /** + * End the uploading message animation + * + * @memberof DotUploadAssetComponent + */ + shakeEnd() { + this.animation = 'shakeend'; + } + + /** + * Shake the uploading message + * + * @private + * @return {*} + * @memberof DotUploadAssetComponent + */ + private shakeMe() { + if (this.animation === 'shakestart') { + return; // already shaking + } + + this.animation = 'shakestart'; } /** @@ -69,13 +145,20 @@ export class DotUploadAssetComponent { * * @memberof DotUploadAssetComponent */ - uploadFile() { + private uploadFile() { + this.controller = new AbortController(); this.status = STATUS.UPLOAD; - this.imageService.publishContent({ data: this.file }).subscribe((data) => { - const contentlet = data[0]; - this.uploadedFile.emit(contentlet[Object.keys(contentlet)[0]]); - this.status = STATUS.SELECT; - }); + this.$uploadRequestSubs = this.imageService + .publishContent({ data: this.file, signal: this.controller.signal }) + .pipe( + take(1), + catchError((error: HttpErrorResponse) => this.handleError(error)) + ) + .subscribe((data) => { + const contentlet = data[0]; + this.uploadedFile.emit(contentlet[Object.keys(contentlet)[0]]); + this.status = STATUS.SELECT; + }); } /** @@ -83,13 +166,42 @@ export class DotUploadAssetComponent { * * @private * @param {File} file - * @param {string | ArrayBuffer} src + * @param {string | ArrayBuffer} buffer * @memberof DotUploadAssetComponent */ - private setFile(file: File, src: string | ArrayBuffer): void { + private setFile(file: File, buffer: string | ArrayBuffer): void { + // Convert the buffer to a blob: + const videoBlob = new Blob([new Uint8Array(buffer as ArrayBufferLike)], { + type: 'video/mp4' + }); + + this.src = this.sanitizer.bypassSecurityTrustResourceUrl(URL.createObjectURL(videoBlob)); this.file = file; - this.src = src; - this.status = STATUS.PREVIEW; + this.status = STATUS.UPLOAD; + this.uploadFile(); this.cd.markForCheck(); } + + /** + * Handle error on upload file. + * + * @private + * @param {HttpErrorResponse} error + * @return {*} + * @memberof DotUploadAssetComponent + */ + private handleError(error: HttpErrorResponse) { + this.status = STATUS.ERROR; + this.preventClose.emit(false); + this.error = error?.error?.errors[0] || error.error; + + console.error(error); + + return throwError(error); + } + + private cancelUploading(): void { + this.$uploadRequestSubs.unsubscribe(); + this.controller?.abort(); + } } diff --git a/core-web/libs/block-editor/src/lib/extensions/image-uploader/image-uploader.extension.ts b/core-web/libs/block-editor/src/lib/extensions/image-uploader/image-uploader.extension.ts index e205c454e4f8..f4c8abb35141 100644 --- a/core-web/libs/block-editor/src/lib/extensions/image-uploader/image-uploader.extension.ts +++ b/core-web/libs/block-editor/src/lib/extensions/image-uploader/image-uploader.extension.ts @@ -76,7 +76,7 @@ export const ImageUpload = (injector: Injector, viewContainerRef: ViewContainerR const placeHolderName = files[0].name; setPlaceHolder(view, position, placeHolderName); dotImageService - .publishContent({ data: files }) + .publishContent({ data: files[0] }) .pipe(take(1)) .subscribe( (dotAssets: DotCMSContentlet[]) => { diff --git a/core-web/libs/block-editor/src/lib/extensions/image-uploader/services/dot-image/dot-image.service.ts b/core-web/libs/block-editor/src/lib/extensions/image-uploader/services/dot-image/dot-image.service.ts index 68d07dd1a9bb..24ba346655a2 100644 --- a/core-web/libs/block-editor/src/lib/extensions/image-uploader/services/dot-image/dot-image.service.ts +++ b/core-web/libs/block-editor/src/lib/extensions/image-uploader/services/dot-image/dot-image.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@angular/core'; -import { uploadFile } from '@dotcms/utils'; + +import { DotUploadService } from '@dotcms/data-access'; import { from, Observable, throwError } from 'rxjs'; import { catchError, pluck, switchMap } from 'rxjs/operators'; import { DotCMSContentlet, DotCMSTempFile } from '@dotcms/dotcms-models'; @@ -13,25 +14,27 @@ export enum FileStatus { } interface PublishContentProps { - data: string | File | File[]; + data: string | File; maxSize?: string; statusCallback?: (status: FileStatus) => void; + signal?: AbortSignal; } @Injectable() export class DotImageService { - constructor(private http: HttpClient) {} + constructor(private http: HttpClient, private dotUploadService: DotUploadService) {} publishContent({ data, maxSize, statusCallback = (_status) => { /* */ - } + }, + signal }: PublishContentProps): Observable { statusCallback(FileStatus.DOWNLOAD); - return this.setTempResource(data, maxSize).pipe( + return this.setTempResource({ data, maxSize, signal }).pipe( switchMap((response: DotCMSTempFile | DotCMSTempFile[]) => { const files = Array.isArray(response) ? response : [response]; const contentlets = []; @@ -63,17 +66,16 @@ export class DotImageService { ); } - private setTempResource( - file: string | File | File[], - maxSize?: string - ): Observable { + private setTempResource({ + data: file, + maxSize, + signal + }: PublishContentProps): Observable { return from( - uploadFile({ + this.dotUploadService.uploadFile({ file, - progressCallBack: () => { - /**/ - }, - maxSize + maxSize, + signal }) ); } diff --git a/core-web/libs/block-editor/src/lib/nodes/image-node/image.node.ts b/core-web/libs/block-editor/src/lib/nodes/image-node/image.node.ts index 6fef70b67b88..3650133078e0 100644 --- a/core-web/libs/block-editor/src/lib/nodes/image-node/image.node.ts +++ b/core-web/libs/block-editor/src/lib/nodes/image-node/image.node.ts @@ -100,7 +100,7 @@ export const ImageNode = Image.extend({ return [ 'div', - { style, class: 'node-container' }, + { style, class: 'node-container node-image' }, href ? imageLinkElement(this.options.HTMLAttributes, HTMLAttributes) : imageElement(this.options.HTMLAttributes, HTMLAttributes) diff --git a/core-web/libs/block-editor/src/lib/nodes/video/video.node.ts b/core-web/libs/block-editor/src/lib/nodes/video/video.node.ts index 6c0c6b1798d8..7408c2310841 100644 --- a/core-web/libs/block-editor/src/lib/nodes/video/video.node.ts +++ b/core-web/libs/block-editor/src/lib/nodes/video/video.node.ts @@ -35,6 +35,13 @@ export const VideoNode = Node.create({ parseHTML: (element) => element.getAttribute('height'), renderHTML: (attributes) => ({ height: attributes.height }) }, + orientation: { + default: null, + parseHTML: (element) => element.getAttribute('orientation'), + renderHTML: ({ height, width }) => ({ + orientation: height > width ? 'vertical' : 'horizontal' + }) + }, data: { default: null, parseHTML: (element) => element.getAttribute('data'), @@ -84,10 +91,18 @@ export const VideoNode = Node.create({ }, renderHTML({ HTMLAttributes }) { + const { orientation = 'horizontal' } = HTMLAttributes; + return [ 'div', { class: 'node-container' }, - ['video', mergeAttributes(HTMLAttributes, { controls: true })] + [ + 'video', + mergeAttributes(HTMLAttributes, { + controls: true, + class: `${orientation}-video` + }) + ] ]; } }); @@ -97,14 +112,18 @@ const getVideoAttrs = (attrs: DotCMSContentlet | string) => { return { src: attrs }; } - const { assetMetaData, asset, assetVersion, mimeType } = attrs; - const { width = 'auto', height = 'auto' } = assetMetaData || {}; + const { assetMetaData, asset, mineType, fileAsset } = attrs; + const { width = 'auto', height = 'auto', contentType } = assetMetaData || {}; + const orientation = height > width ? 'vertical' : 'horizontal'; return { - src: assetVersion || asset, - data: attrs, + src: fileAsset || asset, + data: { + ...attrs + }, width, height, - mimeType + mineType: mineType || contentType, + orientation }; }; diff --git a/core-web/libs/data-access/src/index.ts b/core-web/libs/data-access/src/index.ts new file mode 100644 index 000000000000..7a7e7ba13c7d --- /dev/null +++ b/core-web/libs/data-access/src/index.ts @@ -0,0 +1 @@ +export * from './lib/dot-upload/dot-upload.service'; diff --git a/core-web/libs/dotcms-field-elements/src/components/dot-form/services/dot-upload.service.spec.ts b/core-web/libs/data-access/src/lib/dot-upload/dot-upload.service.spec.ts similarity index 83% rename from core-web/libs/dotcms-field-elements/src/components/dot-form/services/dot-upload.service.spec.ts rename to core-web/libs/data-access/src/lib/dot-upload/dot-upload.service.spec.ts index 8ceb52012cff..99ddb86ded57 100644 --- a/core-web/libs/dotcms-field-elements/src/components/dot-form/services/dot-upload.service.spec.ts +++ b/core-web/libs/data-access/src/lib/dot-upload/dot-upload.service.spec.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { DotUploadService } from './dot-upload.service'; xdescribe('DotUploadService', () => { @@ -19,7 +20,7 @@ xdescribe('DotUploadService', () => { }); it('should send data to the URL endpoint with the correct information', () => { - uploadService.uploadFile('test'); + uploadService.uploadFile({ file: 'test' }); const params = fetchMock.mock.calls[0]; expect(fetchMock.mock.calls.length).toBe(1); @@ -27,7 +28,7 @@ xdescribe('DotUploadService', () => { }); it('should send data to the binary file endpoint without max file size', () => { - uploadService.uploadFile({} as File); + uploadService.uploadFile({ file: {} as File }); const params = fetchMock.mock.calls[0]; expect(fetchMock.mock.calls.length).toBe(1); @@ -35,7 +36,7 @@ xdescribe('DotUploadService', () => { }); it('should send data to the binary file endpoint with max file size', () => { - uploadService.uploadFile({} as File, '1000'); + uploadService.uploadFile({ file: {} as File, maxSize: '1000' }); const params = fetchMock.mock.calls[0]; expect(fetchMock.mock.calls.length).toBe(1); diff --git a/core-web/libs/data-access/src/lib/dot-upload/dot-upload.service.ts b/core-web/libs/data-access/src/lib/dot-upload/dot-upload.service.ts new file mode 100644 index 000000000000..8dcc314e928f --- /dev/null +++ b/core-web/libs/data-access/src/lib/dot-upload/dot-upload.service.ts @@ -0,0 +1,137 @@ +import { Injectable } from '@angular/core'; +import { DotCMSTempFile, DotHttpErrorResponse } from '@dotcms/dotcms-models'; + +export const fallbackErrorMessages: { [key: number]: string } = { + 500: '500 Internal Server Error', + 400: '400 Bad Request', + 401: '401 Unauthorized Error' +}; + +export interface UploadFileProps { + file: string | File; + maxSize?: string; + signal?: AbortSignal; +} + +export interface ErrorResponse { + message: string; + errors: { message: string }[]; +} + +@Injectable({ providedIn: 'root' }) +export class DotUploadService { + /** + * Upload a file to the server + * + * @param {UploadFileProps} {file, maxSize, signal} + * @return {*} {Promise} + * @memberof DotUploadService + */ + uploadFile({ file, maxSize, signal }: UploadFileProps): Promise { + if (typeof file === 'string') { + return this.uploadFileByURL(file, signal); + } else { + return this.uploadBinaryFile({ file, maxSize, signal }); + } + } + + /** + * Upload a file to the server by URL + * + * @private + * @param {string} url + * @param {signal} [AbortSignal] + * @return {*} {Promise} + * @memberof DotUploadService + */ + private uploadFileByURL(url: string, signal?: AbortSignal): Promise { + const UPLOAD_FILE_FROM_URL = '/api/v1/temp/byUrl'; + + return fetch(UPLOAD_FILE_FROM_URL, { + method: 'POST', + signal, + headers: { + 'Content-Type': 'application/json', + Origin: window.location.hostname + }, + body: JSON.stringify({ + remoteUrl: url + }) + }) + .then(async (response: Response) => { + if (response.status === 200) { + return (await response.json()).tempFiles[0]; + } else { + const error: DotHttpErrorResponse = { + message: (await response.json()).message, + status: response.status + }; + throw error; + } + }) + .catch((request) => { + throw this.errorHandler(JSON.parse(request.response), request.status); + }); + } + + /** + * Upload a binary file to the server + * + * @private + * @param {UploadFileProps} {file, maxSize, abortControler} + * @return {*} {Promise} + * @memberof DotUploadService + */ + private uploadBinaryFile({ file, maxSize, signal }: UploadFileProps): Promise { + let path = `/api/v1/temp`; + path += maxSize ? `?maxFileLength=${maxSize}` : ''; + const formData = new FormData(); + formData.append('file', file); + + return fetch(path, { + method: 'POST', + signal, + headers: { + Origin: window.location.hostname + }, + body: formData + }) + .then(async (response: Response) => { + if (response.status === 200) { + return (await response.json()).tempFiles[0]; + } else { + const error: DotHttpErrorResponse = { + message: (await response.json()).message, + status: response.status + }; + throw error; + } + }) + .catch((request) => { + throw this.errorHandler(JSON.parse(request.response), request.status); + }); + } + + /** + * Handle error response + * + * @private + * @param {*} response + * @param {number} status + * @return {*} {DotHttpErrorResponse} + * @memberof DotUploadService + */ + private errorHandler(response: ErrorResponse, status: number): DotHttpErrorResponse { + let message = ''; + try { + message = response.message || response.errors[0].message; + } catch (e) { + message = fallbackErrorMessages[status || 500]; + } + + return { + message: message, + status: status | 500 + }; + } +} diff --git a/core-web/libs/dotcms-field-elements/src/components/dot-form/dot-form.tsx b/core-web/libs/dotcms-field-elements/src/components/dot-form/dot-form.tsx index a8eea6c61ac5..1f64e2acdba1 100644 --- a/core-web/libs/dotcms-field-elements/src/components/dot-form/dot-form.tsx +++ b/core-web/libs/dotcms-field-elements/src/components/dot-form/dot-form.tsx @@ -3,13 +3,14 @@ import { Component, Element, Listen, Prop, State, Watch, h, Host } from '@stenci import { DotFieldStatus } from '../../models'; import { fieldCustomProcess, getFieldsFromLayout, getErrorMessage } from './utils'; import { getClassNames, getOriginalStatus, updateStatus } from '../../utils'; +import { DotUploadService } from '@dotcms/data-access'; + import { DotCMSContentTypeLayoutRow, DotCMSContentTypeField, DotCMSTempFile, DotCMSContentlet } from '@dotcms/dotcms-models'; -import { DotUploadService } from './services/dot-upload.service'; import { DotHttpErrorResponse } from '../../models/dot-http-error-response.model'; import { DotBinaryFileComponent } from '../dot-binary-file/dot-binary-file'; @@ -234,13 +235,13 @@ export class DotFormComponent { const uploadService = new DotUploadService(); const file = event.detail.value; const maxSize = this.getMaxSize(event); - const binary: DotBinaryFileComponent = (event.target as unknown) as DotBinaryFileComponent; + const binary: DotBinaryFileComponent = event.target as unknown as DotBinaryFileComponent; if (!maxSize || file.size <= maxSize) { this.uploadFileInProgress = true; binary.errorMessage = ''; return uploadService - .uploadFile(file, maxSize) + .uploadFile({ file, maxSize }) .then((tempFile: DotCMSTempFile) => { this.errorMessage = ''; binary.previewImageUrl = tempFile.thumbnailUrl; diff --git a/core-web/libs/dotcms-field-elements/src/components/dot-form/services/dot-upload.service.ts b/core-web/libs/dotcms-field-elements/src/components/dot-form/services/dot-upload.service.ts deleted file mode 100644 index 7ad2004bc51d..000000000000 --- a/core-web/libs/dotcms-field-elements/src/components/dot-form/services/dot-upload.service.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { DotCMSTempFile } from '@dotcms/dotcms-models'; -import { DotHttpErrorResponse } from '../../../models/dot-http-error-response.model'; - -export class DotUploadService { - /** - * Will call the corresponding endpoint yo upload a temporary file. - * Return the information of tha file in the server - * @param file - * - * @memberof DotUploadService - */ - uploadFile(file: string | File, maxSize?: string): Promise { - if (typeof file === 'string') { - return this.uploadFileByURL(file); - } else { - return this.uploadBinaryFile(file, maxSize); - } - } - - private uploadFileByURL(url: string): Promise { - const UPLOAD_FILE_FROM_URL = '/api/v1/temp/byUrl'; - return fetch(UPLOAD_FILE_FROM_URL, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Origin: window.location.hostname - }, - body: JSON.stringify({ - remoteUrl: url - }) - }).then(async (response: Response) => { - if (response.status === 200) { - return (await response.json()).tempFiles[0]; - } else { - const error: DotHttpErrorResponse = { - message: (await response.json()).message, - status: response.status - }; - throw error; - } - }); - } - - private uploadBinaryFile(file: File, maxSize?: string): Promise { - let path = `/api/v1/temp`; - path += maxSize ? `?maxFileLength=${maxSize}` : ''; - const formData = new FormData(); - formData.append('file', file); - return fetch(path, { - method: 'POST', - headers: { - Origin: window.location.hostname - }, - body: formData - }).then(async (response: Response) => { - if (response.status === 200) { - return (await response.json()).tempFiles[0]; - } else { - const error: DotHttpErrorResponse = { - message: (await response.json()).message, - status: response.status - }; - throw error; - } - }); - } -} diff --git a/core-web/libs/utils/src/index.ts b/core-web/libs/utils/src/index.ts index 1e56fbf5ac22..fdf6dd291755 100644 --- a/core-web/libs/utils/src/index.ts +++ b/core-web/libs/utils/src/index.ts @@ -1,2 +1 @@ export * from './lib/services/dot-asset.service'; -export * from './lib/services/dot-temp-file.service'; diff --git a/core-web/libs/utils/src/lib/services/dot-asset.service.ts b/core-web/libs/utils/src/lib/services/dot-asset.service.ts index 56f9684486bc..dc90c7bc4168 100644 --- a/core-web/libs/utils/src/lib/services/dot-asset.service.ts +++ b/core-web/libs/utils/src/lib/services/dot-asset.service.ts @@ -4,7 +4,11 @@ import { DotAssetCreateOptions, DotHttpErrorResponse } from '@dotcms/dotcms-models'; -import { fallbackErrorMessages } from './dot-temp-file.service'; +export const fallbackErrorMessages = { + 500: '500 Internal Server Error', + 400: '400 Bad Request', + 401: '401 Unauthorized Error' +}; /** * Create DotAssets based on options passed in DotAssetCreateOptions diff --git a/core-web/libs/utils/src/lib/services/dot-temp-file.service.ts b/core-web/libs/utils/src/lib/services/dot-temp-file.service.ts deleted file mode 100644 index 21a14a669cb6..000000000000 --- a/core-web/libs/utils/src/lib/services/dot-temp-file.service.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { DotCMSTempFile, DotHttpErrorResponse, DotHttpRequestOptions } from '@dotcms/dotcms-models'; - -export const fallbackErrorMessages = { - 500: '500 Internal Server Error', - 400: '400 Bad Request', - 401: '401 Unauthorized Error' -}; - -export interface UploadTempFileProps { - file: T; - progressCallBack?; - maxSize?: string; -} - -const TEMP_API_URL = '/api/v1/temp'; - -/** - * Will call the corresponding endpoint to upload a temporary file. - * Return the information of tha file in the server - * @param file - * @param maxSize - * - */ -export function uploadFile({ - file, - progressCallBack, - maxSize -}: UploadTempFileProps): Promise { - if (typeof file === 'string') { - return uploadFileByURL(file); - } else { - return uploadBinaryFile({ file, progressCallBack, maxSize }) as Promise; - } -} - -function uploadFileByURL(url: string): Promise { - const UPLOAD_FILE_FROM_URL = `${TEMP_API_URL}/byUrl`; - - return fetch(UPLOAD_FILE_FROM_URL, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Origin: window.location.hostname - }, - body: JSON.stringify({ - remoteUrl: url - }) - }).then(async (response: Response) => { - if (response.status === 200) { - return (await response.json()).tempFiles[0]; - } else { - throw errorHandler(await response.json(), response.status); - } - }); -} - -/** - * Will call the temp resource endpoint to upload a temporary file. - * With a callback to track the progress of the upload - * Return the information of tha file(s) in the server - * @param data - * @param progressCallBack - * @param maxSize - * - */ -export function uploadBinaryFile({ - file: data, - progressCallBack, - maxSize -}: UploadTempFileProps): Promise { - let path = TEMP_API_URL; - path += maxSize ? `?maxFileLength=${maxSize}` : ''; - const formData = new FormData(); - - const files = Array.isArray(data) ? data : [data]; - - files.forEach((file: File) => formData.append('files', file)); - - return dotRequest( - path, - { - method: 'POST', - headers: {}, - body: formData - }, - progressCallBack - ) - .then((request: XMLHttpRequest) => { - if (request.status === 200) { - const data = JSON.parse(request.response).tempFiles; - - return data.length > 1 ? data : data[0]; - } else { - throw request; - } - }) - .catch((request: XMLHttpRequest) => { - throw errorHandler(JSON.parse(request.response), request.status); - }); -} - -function dotRequest( - url: string, - opts: DotHttpRequestOptions, - progressCallBack: (progress: number) => { - /* */ - } -): Promise { - return new Promise((res, rej) => { - const xhr = new XMLHttpRequest(); - xhr.open(opts.method || 'get', url); - for (const name in opts.headers || {}) { - xhr.setRequestHeader(name, opts.headers[name]); - } - - xhr.onload = () => res(xhr); - xhr.onerror = rej; - if (xhr.upload && progressCallBack) { - xhr.upload.onprogress = (e: ProgressEvent) => { - const percentComplete = (e.loaded / e.total) * 100; - progressCallBack(percentComplete); - }; - } - - xhr.send(opts.body); - }); -} - -function errorHandler(response: Record, status: number): DotHttpErrorResponse { - let message = ''; - try { - message = response.message || fallbackErrorMessages[status]; - } catch (e) { - message = fallbackErrorMessages[status || 500]; - } - - return { - message: message, - status: status | 500 - }; -}