diff --git a/ui-ngx/src/app/core/http/resource.service.ts b/ui-ngx/src/app/core/http/resource.service.ts index 168c63b3b1..81c20be472 100644 --- a/ui-ngx/src/app/core/http/resource.service.ts +++ b/ui-ngx/src/app/core/http/resource.service.ts @@ -17,7 +17,7 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { PageLink } from '@shared/models/page/page-link'; -import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; +import { defaultHttpOptionsFromConfig, defaultHttpUploadOptions, RequestConfig } from '@core/http/http-utils'; import { forkJoin, Observable, of } from 'rxjs'; import { PageData } from '@shared/models/page/page-data'; import { Resource, ResourceInfo, ResourceSubType, ResourceType, TBResourceScope } from '@shared/models/resource.models'; @@ -90,6 +90,54 @@ export class ResourceService { return this.http.post('/api/resource', resource, defaultHttpOptionsFromConfig(config)); } + public uploadResources(resources: Resource[], config?: RequestConfig): Observable { + let partSize = 100; + partSize = resources.length > partSize ? partSize : resources.length; + const resourceObservables: Observable[] = []; + for (let i = 0; i < partSize; i++) { + resourceObservables.push(this.uploadResource(resources[i], config).pipe(catchError(() => of({} as Resource)))); + } + return forkJoin(resourceObservables).pipe( + mergeMap((resource) => { + resources.splice(0, partSize); + if (resources.length) { + return this.uploadResources(resources, config); + } else { + return of(resource); + } + }) + ); + } + + public uploadResource(resource: Resource, config?: RequestConfig): Observable { + if (!config) { + config = {}; + } + const formData = new FormData(); + formData.append('file', resource.data); + formData.append('title', resource.title); + formData.append('resourceType', resource.resourceType); + if (resource.resourceSubType) { + formData.append('resourceSubType', resource.resourceSubType); + } + return this.http.post('/api/resource/upload', formData, + defaultHttpUploadOptions(config.ignoreLoading, config.ignoreErrors, config.resendRequest)); + } + + public updatedResourceInfo(resourceId: string, updatedResources: Partial>, config?: RequestConfig): Observable { + return this.http.put(`/api/resource/${resourceId}/info`, updatedResources, defaultHttpOptionsFromConfig(config)); + } + + public updatedResourceData(resourceId: string, data: File, config?: RequestConfig): Observable { + if (!config) { + config = {}; + } + const formData = new FormData(); + formData.append('file', data); + return this.http.put(`/api/resource/${resourceId}/data`, formData, + defaultHttpUploadOptions(config.ignoreLoading, config.ignoreErrors, config.resendRequest)); + } + public deleteResource(resourceId: string, force = false, config?: RequestConfig) { return this.http.delete(`/api/resource/${resourceId}?force=${force}`, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.ts b/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.ts index 6216f087b1..3848879dc5 100644 --- a/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.ts @@ -98,18 +98,17 @@ export class ResourcesDialogComponent extends DialogComponent response[0]) ).subscribe(result => this.dialogRef.close(result)); } else { if (resource.resourceType !== ResourceType.GENERAL) { delete resource.descriptor; } - this.resourceService.saveResource(resource).subscribe(result => this.dialogRef.close(result)); + this.resourceService.uploadResource(resource).subscribe(result => this.dialogRef.close(result)); } } } diff --git a/ui-ngx/src/app/modules/home/components/resources/resources-library.component.html b/ui-ngx/src/app/modules/home/components/resources/resources-library.component.html index 819753e105..8fb61d2af2 100644 --- a/ui-ngx/src/app/modules/home/components/resources/resources-library.component.html +++ b/ui-ngx/src/app/modules/home/components/resources/resources-library.component.html @@ -70,7 +70,7 @@ impleme return this.fb.group({ title: [entity ? entity.title : '', [Validators.required, Validators.maxLength(255)]], resourceType: [entity?.resourceType ? entity.resourceType : ResourceType.LWM2M_MODEL, Validators.required], - fileName: [entity ? entity.fileName : null, Validators.required], + fileName: [entity ? entity.fileName : null], data: [entity ? entity.data : null, this.isAdd ? [Validators.required] : []], descriptor: this.fb.group({ mediaType: [''] diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-config.resolver.ts index 341bbd2e06..c605018f71 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-config.resolver.ts @@ -32,7 +32,7 @@ import { ResourceType, toResourceDeleteResult } from '@shared/models/resource.models'; -import { EntityType, entityTypeResources } from '@shared/models/entity-type.models'; +import { EntityType } from '@shared/models/entity-type.models'; import { NULL_UUID } from '@shared/models/id/has-uuid'; import { DatePipe } from '@angular/common'; import { TranslateService } from '@ngx-translate/core'; @@ -47,7 +47,7 @@ import { JsLibraryTableHeaderComponent } from '@home/pages/admin/resource/js-lib import { JsResourceComponent } from '@home/pages/admin/resource/js-resource.component'; import { catchError, map, switchMap } from 'rxjs/operators'; import { ResourceTabsComponent } from '@home/pages/admin/resource/resource-tabs.component'; -import { forkJoin, of } from 'rxjs'; +import { forkJoin, Observable, of } from 'rxjs'; import { parseHttpErrorMessage } from '@core/utils'; import { ActionNotificationShow } from '@core/notification/notification.actions'; import { MatDialog } from '@angular/material/dialog'; @@ -118,9 +118,20 @@ export class JsLibraryTableConfigResolver { return this.resourceService.getResourceInfoById(id.id) } }; - this.config.saveEntity = resource => { + this.config.saveEntity = (resource: Resource, originalResource: Resource) => { resource.resourceType = ResourceType.JS_MODULE; - let saveObservable = this.resourceService.saveResource(resource); + let saveObservable: Observable; + if (!originalResource) { + saveObservable = this.resourceService.uploadResource(resource); + } else { + const { data, ...resourceInfo } = resource; + saveObservable = this.resourceService.updatedResourceInfo(resource.id.id, resourceInfo); + if (data) { + saveObservable = saveObservable.pipe( + switchMap(() => this.resourceService.updatedResourceData(resource.id.id, data)) + ) + } + } if (resource.resourceSubType === ResourceSubType.MODULE) { saveObservable = saveObservable.pipe( switchMap((saved) => this.resourceService.getResource(saved.id.id)) diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.html b/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.html index c9f7483bdc..fd7fd7ee6b 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.html @@ -70,7 +70,7 @@ formControlName="data" [required]="isAdd" label="{{ 'javascript.resource-file' | translate }}" - [readAsBinary]="true" + [workFromFileObj]="true" [maxSizeByte]="maxResourceSize" [allowedExtensions]="getAllowedExtensions()" [contentConvertFunction]="convertToBase64File" diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.ts index 4a67bd10c8..c8e9ccc82e 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.ts @@ -32,7 +32,7 @@ import { } from '@shared/models/resource.models'; import { startWith, takeUntil } from 'rxjs/operators'; import { ActionNotificationShow } from '@core/notification/notification.actions'; -import { isDefinedAndNotNull } from '@core/utils'; +import { base64toString, isDefinedAndNotNull, stringToBase64 } from '@core/utils'; import { getCurrentAuthState } from '@core/auth/auth.selectors'; @Component({ @@ -82,15 +82,15 @@ export class JsResourceComponent extends EntityComponent implements On return this.fb.group({ title: [entity ? entity.title : '', [Validators.required, Validators.maxLength(255)]], resourceSubType: [entity?.resourceSubType ? entity.resourceSubType : ResourceSubType.EXTENSION, Validators.required], - fileName: [entity ? entity.fileName : null, Validators.required], + fileName: [entity ? entity.fileName : null], data: [entity ? entity.data : null, this.isAdd ? [Validators.required] : []], - content: [entity?.data?.length ? window.atob(entity.data) : '', Validators.required] + content: [entity?.data?.length ? base64toString(entity.data) : '', Validators.required] }); } updateForm(entity: Resource): void { this.entityForm.patchValue(entity); - const content = entity.resourceSubType === ResourceSubType.MODULE && entity?.data?.length ? window.atob(entity.data) : ''; + const content = entity.resourceSubType === ResourceSubType.MODULE && entity?.data?.length ? base64toString(entity.data) : ''; this.entityForm.get('content').patchValue(content); } @@ -110,7 +110,9 @@ export class JsResourceComponent extends EntityComponent implements On if (!formValue.fileName) { formValue.fileName = formValue.title + '.js'; } - formValue.data = window.btoa((formValue as any).content); + formValue.data = new File([(formValue as any).content], formValue.fileName, { + type: 'text/javascript' + }); delete (formValue as any).content; } return super.prepareFormValue(formValue); @@ -125,7 +127,7 @@ export class JsResourceComponent extends EntityComponent implements On } convertToBase64File(data: string): string { - return window.btoa(data); + return stringToBase64(data); } onResourceIdCopied(): void { diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts index 1bce24f984..0499cb2d1b 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts @@ -24,7 +24,8 @@ import { import { Router } from '@angular/router'; import { Resource, - ResourceInfo, ResourceInfoWithReferences, + ResourceInfo, + ResourceInfoWithReferences, ResourceType, ResourceTypeTranslationMap, toResourceDeleteResult @@ -41,10 +42,10 @@ import { Authority } from '@shared/models/authority.enum'; import { ResourcesLibraryComponent } from '@home/components/resources/resources-library.component'; import { PageLink } from '@shared/models/page/page-link'; import { EntityAction } from '@home/models/entity/entity-component.models'; -import { catchError, map } from 'rxjs/operators'; +import { catchError, map, switchMap } from 'rxjs/operators'; import { ResourcesTableHeaderComponent } from '@home/pages/admin/resource/resources-table-header.component'; import { ResourceLibraryTabsComponent } from '@home/pages/admin/resource/resource-library-tabs.component'; -import { forkJoin, of } from "rxjs"; +import { forkJoin, Observable, of } from "rxjs"; import { ResourcesInUseDialogComponent, ResourcesInUseDialogData @@ -114,27 +115,36 @@ export class ResourcesLibraryTableConfigResolver { this.config.entitiesFetchFunction = pageLink => this.resourceService.getResources(pageLink, this.config.componentsData.resourceType); this.config.loadEntity = id => this.resourceService.getResourceInfoById(id.id); - this.config.saveEntity = resource => this.saveResource(resource); + this.config.saveEntity = (resource, originalResource) => this.saveResource(resource, originalResource); this.config.onEntityAction = action => this.onResourceAction(action); } - saveResource(resource) { + saveResource(resource: Resource & {data?: File | File[]}, originalResource: Resource) { if (Array.isArray(resource.data)) { const resources = []; resource.data.forEach((data, index) => { resources.push({ resourceType: resource.resourceType, data, - fileName: resource.fileName[index], title: resource.title }); }); - return this.resourceService.saveResources(resources, {resendRequest: true}).pipe( + return this.resourceService.uploadResources(resources, {resendRequest: true}).pipe( map((response) => response[0]) ); + } else if (!originalResource) { + return this.resourceService.uploadResource(resource); } else { - return this.resourceService.saveResource(resource); + const { data, ...resourceInfo } = resource; + let saveObservable: Observable; + saveObservable = this.resourceService.updatedResourceInfo(resource.id.id, resourceInfo); + if (data) { + saveObservable = saveObservable.pipe( + switchMap(() => this.resourceService.updatedResourceData(resource.id.id, data)) + ) + } + return saveObservable; } } diff --git a/ui-ngx/src/app/shared/components/file-input.component.ts b/ui-ngx/src/app/shared/components/file-input.component.ts index 6960db73dd..518e5920c1 100644 --- a/ui-ngx/src/app/shared/components/file-input.component.ts +++ b/ui-ngx/src/app/shared/components/file-input.component.ts @@ -180,17 +180,18 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, if (readers.length) { Promise.all(readers).then((files) => { - files = files.filter(file => file.fileContent != null || file.files != null); - if (files.length === 1) { - this.fileContent = files[0].fileContent; - this.fileName = files[0].fileName; - this.files = files[0].files; - this.mediaType = files[0].mediaType; + const validResults = files.filter(file => file.fileContent != null || file.files != null); + + if (validResults.length === 1) { + this.fileContent = validResults[0].fileContent; + this.fileName = validResults[0].fileName; + this.files = validResults[0].files; + this.mediaType = validResults[0].mediaType; this.updateModel(); - } else if (files.length > 1) { - this.fileContent = files.map(content => content.fileContent); - this.fileName = files.map(content => content.fileName); - this.files = files.map(content => content.files); + } else if (validResults.length > 1) { + this.fileContent = validResults.map(content => content.fileContent); + this.fileName = validResults.map(content => content.fileName); + this.files = validResults.map(content => content.files); this.updateModel(); } }); @@ -204,29 +205,32 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, private readerAsFile(file: flowjs.FlowFile): Promise { return new Promise((resolve) => { + if (this.workFromFileObj) { + resolve({ + fileContent: null, + fileName: file.name, + files: file.file, + mediaType: file.file.type || null + }); + return; + } + const reader = new FileReader(); reader.onload = () => { let fileName = null; let fileContent = null; - let files = null; let mediaType = null; if (reader.readyState === reader.DONE) { - if (!this.workFromFileObj) { - fileContent = reader.result; - if (fileContent && fileContent.length > 0) { - if (this.contentConvertFunction) { - fileContent = this.contentConvertFunction(fileContent); - } - fileName = fileContent ? file.name : null; - mediaType = file?.file?.type || null; + fileContent = reader.result; + if (fileContent && fileContent.length > 0) { + if (this.contentConvertFunction) { + fileContent = this.contentConvertFunction(fileContent); } - } else if (file.name || file.file){ - files = file.file; - fileName = file.name; - mediaType = file.file.type || null; + fileName = fileContent ? file.name : null; + mediaType = file?.file?.type || null; } } - resolve({fileContent, fileName, files, mediaType}); + resolve({fileContent, fileName, files: null, mediaType}); }; reader.onerror = () => { resolve({fileContent: null, fileName: null, files: null, mediaType: null}); diff --git a/ui-ngx/src/app/shared/models/resource.models.ts b/ui-ngx/src/app/shared/models/resource.models.ts index e19f1c82a0..be25cb7871 100644 --- a/ui-ngx/src/app/shared/models/resource.models.ts +++ b/ui-ngx/src/app/shared/models/resource.models.ts @@ -89,7 +89,7 @@ export interface TbResourceInfo extends Omit, 'name' | export type ResourceInfo = TbResourceInfo; export interface Resource extends ResourceInfo { - data?: string; + data?: any; name?: string; }