diff --git a/ui-ngx/src/app/core/http/resource.service.ts b/ui-ngx/src/app/core/http/resource.service.ts index d8bc18291b..6d5a367c86 100644 --- a/ui-ngx/src/app/core/http/resource.service.ts +++ b/ui-ngx/src/app/core/http/resource.service.ts @@ -18,10 +18,10 @@ 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 { Observable } from 'rxjs'; +import { forkJoin, Observable, of } from 'rxjs'; import { PageData } from '@shared/models/page/page-data'; import { Resource, ResourceInfo } from '@shared/models/resource.models'; -import { map } from 'rxjs/operators'; +import { catchError, map, mergeMap } from 'rxjs/operators'; @Injectable({ providedIn: 'root' @@ -70,6 +70,25 @@ export class ResourceService { ); } + public saveResources(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.saveResource(resources[i], config).pipe(catchError(() => of({} as Resource)))); + } + return forkJoin(resourceObservables).pipe( + mergeMap((resource) => { + resources.splice(0, partSize); + if (resources.length) { + return this.saveResources(resources, config); + } else { + return of(resource); + } + }) + ); + } + public saveResource(resource: Resource, config?: RequestConfig): Observable { return this.http.post('/api/resource', resource, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/modules/home/pages/resource/resources-library-table-config.resolve.ts b/ui-ngx/src/app/modules/home/pages/resource/resources-library-table-config.resolve.ts index 55dc474a3d..d6759310ac 100644 --- a/ui-ngx/src/app/modules/home/pages/resource/resources-library-table-config.resolve.ts +++ b/ui-ngx/src/app/modules/home/pages/resource/resources-library-table-config.resolve.ts @@ -24,7 +24,6 @@ import { import { Resolve } from '@angular/router'; import { Resource, ResourceInfo, ResourceTypeTranslationMap } from '@shared/models/resource.models'; import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; -import { Direction } from '@shared/models/page/sort-order'; import { NULL_UUID } from '@shared/models/id/has-uuid'; import { DatePipe } from '@angular/common'; import { TranslateService } from '@ngx-translate/core'; @@ -34,13 +33,14 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { Authority } from '@shared/models/authority.enum'; import { ResourcesLibraryComponent } from '@home/pages/resource/resources-library.component'; -import { Observable } from 'rxjs/internal/Observable'; -import { PageData } from '@shared/models/page/page-data'; +import { PageLink } from '@shared/models/page/page-link'; +import { EntityAction } from '@home/models/entity/entity-component.models'; +import { map } from 'rxjs/operators'; @Injectable() -export class ResourcesLibraryTableConfigResolver implements Resolve> { +export class ResourcesLibraryTableConfigResolver implements Resolve> { - private readonly config: EntityTableConfig = new EntityTableConfig(); + private readonly config: EntityTableConfig = new EntityTableConfig(); private readonly resourceTypesTranslationMap = ResourceTypeTranslationMap; constructor(private store: Store, @@ -52,17 +52,16 @@ export class ResourcesLibraryTableConfigResolver implements Resolve resource ? resource.title : ''; this.config.columns.push( new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px'), - new EntityTableColumn('title', 'widgets-bundle.title', '60%'), + new EntityTableColumn('title', 'resource.title', '60%'), new EntityTableColumn('resourceType', 'resource.resource-type', '40%', entity => this.resourceTypesTranslationMap.get(entity.resourceType)), - new EntityTableColumn('tenantId', 'widgets-bundle.system', '60px', + new EntityTableColumn('tenantId', 'resource.system', '60px', entity => { return checkBoxCell(entity.tenantId.id === NULL_UUID); }), @@ -83,13 +82,34 @@ export class ResourcesLibraryTableConfigResolver implements Resolve this.translate.instant('resource.delete-resources-title', {count}); this.config.deleteEntitiesContent = () => this.translate.instant('resource.delete-resources-text'); - this.config.entitiesFetchFunction = pageLink => this.resourceService.getResources(pageLink) as Observable>; + this.config.entitiesFetchFunction = pageLink => this.resourceService.getResources(pageLink); this.config.loadEntity = id => this.resourceService.getResource(id.id); - this.config.saveEntity = resource => this.resourceService.saveResource(resource); + this.config.saveEntity = resource => this.saveResource(resource); this.config.deleteEntity = id => this.resourceService.deleteResource(id.id); + + this.config.onEntityAction = action => this.onResourceAction(action); } - resolve(): EntityTableConfig { + saveResource(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( + map((response) => response[0]) + ); + } else { + return this.resourceService.saveResource(resource); + } + } + + resolve(): EntityTableConfig { this.config.tableTitle = this.translate.instant('resource.resources-library'); const authUser = getCurrentAuthUser(this.store); this.config.deleteEnabled = (resource) => this.isResourceEditable(resource, authUser.authority); @@ -105,7 +125,16 @@ export class ResourcesLibraryTableConfigResolver implements Resolve): boolean { + switch (action.action) { + case 'uploadResource': + this.exportResource(action.event, action.entity); + return true; + } + return false; + } + + private isResourceEditable(resource: ResourceInfo, authority: Authority): boolean { if (authority === Authority.TENANT_ADMIN) { return resource && resource.tenantId && resource.tenantId.id !== NULL_UUID; } else { diff --git a/ui-ngx/src/app/modules/home/pages/resource/resources-library.component.html b/ui-ngx/src/app/modules/home/pages/resource/resources-library.component.html index e4fa600f9c..5b98fa9c61 100644 --- a/ui-ngx/src/app/modules/home/pages/resource/resources-library.component.html +++ b/ui-ngx/src/app/modules/home/pages/resource/resources-library.component.html @@ -16,6 +16,12 @@ -->
+
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 b91fba1def..0fcaf8b790 100644 --- a/ui-ngx/src/app/shared/components/file-input.component.ts +++ b/ui-ngx/src/app/shared/components/file-input.component.ts @@ -17,6 +17,7 @@ import { AfterViewInit, Component, + ElementRef, EventEmitter, forwardRef, Input, @@ -102,17 +103,34 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, existingFileName: string; @Input() - convertToBase64 = false; + readAsBinary = false; + + private multipleFileValue = false; + + @Input() + set multipleFile(value: boolean) { + this.multipleFileValue = value; + if (this.flow?.flowJs) { + this.updateMultipleFileMode(this.multipleFile); + } + } + + get multipleFile(): boolean { + return this.multipleFileValue; + } @Output() - fileNameChanged = new EventEmitter(); + fileNameChanged = new EventEmitter(); - fileName: string; + fileName: string | string[]; fileContent: any; @ViewChild('flow', {static: true}) flow: FlowDirective; + @ViewChild('flowInput', {static: true}) + flowInput: ElementRef; + autoUploadSubscription: Subscription; private propagateChange = null; @@ -125,34 +143,60 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, ngAfterViewInit() { this.autoUploadSubscription = this.flow.events$.subscribe(event => { - if (event.type === 'fileAdded') { - const file = event.event[0] as flowjs.FlowFile; - if (this.filterFile(file)) { - const reader = new FileReader(); - reader.onload = (loadEvent) => { - if (typeof reader.result === 'string') { - const fileContent = this.convertToBase64 ? window.btoa(reader.result) : reader.result; - if (fileContent && fileContent.length > 0) { - if (this.contentConvertFunction) { - this.fileContent = this.contentConvertFunction(fileContent); - } else { - this.fileContent = fileContent; - } - if (this.fileContent) { - this.fileName = file.name; - } else { - this.fileName = null; - } - this.updateModel(); - } + if (event.type === 'filesAdded') { + const readers = []; + (event.event[0] as flowjs.FlowFile[]).forEach(file => { + if (this.filterFile(file)) { + readers.push(this.readerAsFile(file)); + } + }); + if (readers.length) { + Promise.all(readers).then((filesContent) => { + filesContent = filesContent.filter(content => content.fileContent != null); + if (filesContent.length === 1) { + this.fileContent = filesContent[0].fileContent; + this.fileName = filesContent[0].fileName; + this.updateModel(); + } else if (filesContent.length > 1) { + this.fileContent = filesContent.map(content => content.fileContent); + this.fileName = filesContent.map(content => content.fileName); + this.updateModel(); + } + }); + } + } + }); + if (!this.multipleFile) { + this.updateMultipleFileMode(this.multipleFile); + } + } + + private readerAsFile(file: flowjs.FlowFile): Promise { + return new Promise((resolve) => { + const reader = new FileReader(); + reader.onload = () => { + let fileName = null; + let fileContent = null; + if (typeof reader.result === 'string') { + fileContent = reader.result; + if (fileContent && fileContent.length > 0) { + if (this.contentConvertFunction) { + fileContent = this.contentConvertFunction(fileContent); + } + if (fileContent) { + fileName = file.name; } - }; - if (this.convertToBase64) { - reader.readAsBinaryString(file.file); - } else { - reader.readAsText(file.file); } } + resolve({fileContent, fileName}); + }; + reader.onerror = () => { + resolve({fileContent: null, fileName: null}); + }; + if (this.readAsBinary) { + reader.readAsBinaryString(file.file); + } else { + reader.readAsText(file.file); } }); } @@ -207,4 +251,11 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, this.fileContent = null; this.updateModel(); } + + private updateMultipleFileMode(multiple: boolean) { + this.flow.flowJs.opts.singleFile = !multiple; + if (!multiple) { + this.flowInput.nativeElement.removeAttribute('multiple'); + } + } } diff --git a/ui-ngx/src/app/shared/models/resource.models.ts b/ui-ngx/src/app/shared/models/resource.models.ts index 805a046673..154f3c65f9 100644 --- a/ui-ngx/src/app/shared/models/resource.models.ts +++ b/ui-ngx/src/app/shared/models/resource.models.ts @@ -59,3 +59,8 @@ export interface Resource extends ResourceInfo { data: string; fileName: string; } + +export interface Resources extends ResourceInfo { + data: string|string[]; + fileName: string|string[]; +}