Merge pull request #4391 from vvlladd28/feature/multiple-file-input
UI: Added multiple-file-input; Add support load multiple resource
This commit is contained in:
		
						commit
						1142502e6a
					
				@ -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<Resource[]> {
 | 
			
		||||
    let partSize = 100;
 | 
			
		||||
    partSize = resources.length > partSize ? partSize : resources.length;
 | 
			
		||||
    const resourceObservables: Observable<Resource>[] = [];
 | 
			
		||||
    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<Resource> {
 | 
			
		||||
    return this.http.post<Resource>('/api/resource', resource, defaultHttpOptionsFromConfig(config));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -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<EntityTableConfig<Resource>> {
 | 
			
		||||
export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableConfig<Resource, PageLink, ResourceInfo>> {
 | 
			
		||||
 | 
			
		||||
  private readonly config: EntityTableConfig<Resource> = new EntityTableConfig<Resource>();
 | 
			
		||||
  private readonly config: EntityTableConfig<Resource, PageLink, ResourceInfo> = new EntityTableConfig<Resource, PageLink, ResourceInfo>();
 | 
			
		||||
  private readonly resourceTypesTranslationMap = ResourceTypeTranslationMap;
 | 
			
		||||
 | 
			
		||||
  constructor(private store: Store<AppState>,
 | 
			
		||||
@ -52,17 +52,16 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
 | 
			
		||||
    this.config.entityComponent = ResourcesLibraryComponent;
 | 
			
		||||
    this.config.entityTranslations = entityTypeTranslations.get(EntityType.TB_RESOURCE);
 | 
			
		||||
    this.config.entityResources = entityTypeResources.get(EntityType.TB_RESOURCE);
 | 
			
		||||
    this.config.defaultSortOrder = {property: 'title', direction: Direction.ASC};
 | 
			
		||||
 | 
			
		||||
    this.config.entityTitle = (resource) => resource ?
 | 
			
		||||
      resource.title : '';
 | 
			
		||||
 | 
			
		||||
    this.config.columns.push(
 | 
			
		||||
      new DateEntityTableColumn<ResourceInfo>('createdTime', 'common.created-time', this.datePipe, '150px'),
 | 
			
		||||
      new EntityTableColumn<ResourceInfo>('title', 'widgets-bundle.title', '60%'),
 | 
			
		||||
      new EntityTableColumn<ResourceInfo>('title', 'resource.title', '60%'),
 | 
			
		||||
      new EntityTableColumn<ResourceInfo>('resourceType', 'resource.resource-type', '40%',
 | 
			
		||||
        entity => this.resourceTypesTranslationMap.get(entity.resourceType)),
 | 
			
		||||
      new EntityTableColumn<ResourceInfo>('tenantId', 'widgets-bundle.system', '60px',
 | 
			
		||||
      new EntityTableColumn<ResourceInfo>('tenantId', 'resource.system', '60px',
 | 
			
		||||
        entity => {
 | 
			
		||||
          return checkBoxCell(entity.tenantId.id === NULL_UUID);
 | 
			
		||||
        }),
 | 
			
		||||
@ -83,13 +82,34 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
 | 
			
		||||
    this.config.deleteEntitiesTitle = count => 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<PageData<Resource>>;
 | 
			
		||||
    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<Resource> {
 | 
			
		||||
  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<Resource, PageLink, ResourceInfo> {
 | 
			
		||||
    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<EntityTableC
 | 
			
		||||
    this.resourceService.downloadResource(resource.id.id).subscribe();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private isResourceEditable(resource: Resource, authority: Authority): boolean {
 | 
			
		||||
  onResourceAction(action: EntityAction<ResourceInfo>): 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 {
 | 
			
		||||
 | 
			
		||||
@ -16,6 +16,12 @@
 | 
			
		||||
 | 
			
		||||
-->
 | 
			
		||||
<div class="tb-details-buttons" fxLayout.xs="column">
 | 
			
		||||
  <button mat-raised-button color="primary" fxFlex.xs
 | 
			
		||||
          [disabled]="(isLoading$ | async)"
 | 
			
		||||
          (click)="onEntityAction($event, 'uploadResource')"
 | 
			
		||||
          [fxShow]="!isEdit">
 | 
			
		||||
    {{'resource.export' | translate }}
 | 
			
		||||
  </button>
 | 
			
		||||
  <button mat-raised-button color="primary" fxFlex.xs
 | 
			
		||||
          [disabled]="(isLoading$ | async)"
 | 
			
		||||
          (click)="onEntityAction($event, 'delete')"
 | 
			
		||||
@ -44,9 +50,11 @@
 | 
			
		||||
      <tb-file-input
 | 
			
		||||
        formControlName="data"
 | 
			
		||||
        required
 | 
			
		||||
        [convertToBase64]="true"
 | 
			
		||||
        [readAsBinary]="true"
 | 
			
		||||
        [allowedExtensions]="getAllowedExtensions()"
 | 
			
		||||
        [contentConvertFunction]="convertToBase64File"
 | 
			
		||||
        [accept]="getAcceptType()"
 | 
			
		||||
        [multipleFile]="entityForm.get('resourceType').value === resourceType.LWM2M_MODEL"
 | 
			
		||||
        dropLabel="{{'resource.drop-file' | translate}}"
 | 
			
		||||
        [existingFileName]="entityForm.get('fileName')?.value"
 | 
			
		||||
        (fileNameChanged)="entityForm?.get('fileName').patchValue($event)">
 | 
			
		||||
 | 
			
		||||
@ -29,7 +29,7 @@ import {
 | 
			
		||||
  ResourceTypeMIMETypes,
 | 
			
		||||
  ResourceTypeTranslationMap
 | 
			
		||||
} from '@shared/models/resource.models';
 | 
			
		||||
import { distinctUntilChanged, takeUntil } from 'rxjs/operators';
 | 
			
		||||
import { pairwise, startWith, takeUntil } from 'rxjs/operators';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'tb-resources-library',
 | 
			
		||||
@ -54,15 +54,22 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
    super.ngOnInit();
 | 
			
		||||
    this.entityForm.get('resourceType').valueChanges.pipe(
 | 
			
		||||
      distinctUntilChanged((oldValue, newValue) => [oldValue, newValue].includes(this.resourceType.LWM2M_MODEL)),
 | 
			
		||||
      startWith(ResourceType.LWM2M_MODEL),
 | 
			
		||||
      pairwise(),
 | 
			
		||||
      takeUntil(this.destroy$)
 | 
			
		||||
    ).subscribe((type) => {
 | 
			
		||||
    ).subscribe(([previousType, type]) => {
 | 
			
		||||
      if (previousType === this.resourceType.LWM2M_MODEL) {
 | 
			
		||||
        this.entityForm.get('title').setValidators(Validators.required);
 | 
			
		||||
        this.entityForm.get('title').updateValueAndValidity({emitEvent: false});
 | 
			
		||||
      }
 | 
			
		||||
      if (type === this.resourceType.LWM2M_MODEL) {
 | 
			
		||||
        this.entityForm.get('title').clearValidators();
 | 
			
		||||
      } else {
 | 
			
		||||
        this.entityForm.get('title').setValidators(Validators.required);
 | 
			
		||||
        this.entityForm.get('title').updateValueAndValidity({emitEvent: false});
 | 
			
		||||
      }
 | 
			
		||||
      this.entityForm.get('title').updateValueAndValidity({emitEvent: false});
 | 
			
		||||
      this.entityForm.patchValue({
 | 
			
		||||
        data: null,
 | 
			
		||||
        fileName: null
 | 
			
		||||
      }, {emitEvent: false});
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -119,4 +126,8 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
 | 
			
		||||
      return '*/*';
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  convertToBase64File(data: string): string {
 | 
			
		||||
    return window.btoa(data);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -18,7 +18,7 @@
 | 
			
		||||
<div class="tb-container">
 | 
			
		||||
  <label class="tb-title">{{ label }}</label>
 | 
			
		||||
  <ng-container #flow="flow"
 | 
			
		||||
                [flowConfig]="{singleFile: true, allowDuplicateUploads: true}">
 | 
			
		||||
                [flowConfig]="{allowDuplicateUploads: true}">
 | 
			
		||||
    <div class="tb-file-select-container">
 | 
			
		||||
      <div class="tb-file-clear-container">
 | 
			
		||||
        <button mat-button mat-icon-button color="primary"
 | 
			
		||||
@ -34,7 +34,7 @@
 | 
			
		||||
           flowDrop
 | 
			
		||||
           [flow]="flow.flowJs">
 | 
			
		||||
        <label for="{{inputId}}">{{ dropLabel }}</label>
 | 
			
		||||
        <input class="file-input" flowButton type="file" [flow]="flow.flowJs" [flowAttributes]="{accept: accept}" id="{{inputId}}">
 | 
			
		||||
        <input class="file-input" flowButton #flowInput type="file" [flow]="flow.flowJs" [flowAttributes]="{accept: accept}" id="{{inputId}}">
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </ng-container>
 | 
			
		||||
 | 
			
		||||
@ -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<string>();
 | 
			
		||||
  fileNameChanged = new EventEmitter<string|string[]>();
 | 
			
		||||
 | 
			
		||||
  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<any> {
 | 
			
		||||
    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');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -59,3 +59,8 @@ export interface Resource extends ResourceInfo {
 | 
			
		||||
  data: string;
 | 
			
		||||
  fileName: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Resources extends ResourceInfo {
 | 
			
		||||
  data: string|string[];
 | 
			
		||||
  fileName: string|string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user