Commit 2e5bbe2032d446d695f0c702cd1b881a7dd3b5e7

Authored by Vladyslav_Prykhodko
2 parents 95c95624 5672d2c4

Merge remote-tracking branch 'origin/feature/multiple-file-input' into YevhenBon…

…darenko-develop/3.3-firmware
@@ -18,10 +18,10 @@ import { Injectable } from '@angular/core'; @@ -18,10 +18,10 @@ import { Injectable } from '@angular/core';
18 import { HttpClient } from '@angular/common/http'; 18 import { HttpClient } from '@angular/common/http';
19 import { PageLink } from '@shared/models/page/page-link'; 19 import { PageLink } from '@shared/models/page/page-link';
20 import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; 20 import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils';
21 -import { Observable } from 'rxjs'; 21 +import { forkJoin, Observable, of } from 'rxjs';
22 import { PageData } from '@shared/models/page/page-data'; 22 import { PageData } from '@shared/models/page/page-data';
23 import { Resource, ResourceInfo } from '@shared/models/resource.models'; 23 import { Resource, ResourceInfo } from '@shared/models/resource.models';
24 -import { map } from 'rxjs/operators'; 24 +import { catchError, map, mergeMap } from 'rxjs/operators';
25 25
26 @Injectable({ 26 @Injectable({
27 providedIn: 'root' 27 providedIn: 'root'
@@ -70,6 +70,25 @@ export class ResourceService { @@ -70,6 +70,25 @@ export class ResourceService {
70 ); 70 );
71 } 71 }
72 72
  73 + public saveResources(resources: Resource[], config?: RequestConfig) {
  74 + let partSize = 100;
  75 + partSize = resources.length > partSize ? partSize : resources.length;
  76 + const resourceObservables = [];
  77 + for (let i = 0; i < partSize; i++) {
  78 + resourceObservables.push(this.saveResource(resources[i], config).pipe(catchError(error => of(error))));
  79 + }
  80 + return forkJoin(resourceObservables).pipe(
  81 + mergeMap((resource) => {
  82 + resources.splice(0, partSize);
  83 + if (resources.length) {
  84 + return this.saveResources(resources, config);
  85 + } else {
  86 + return of(resource);
  87 + }
  88 + })
  89 + );
  90 + }
  91 +
73 public saveResource(resource: Resource, config?: RequestConfig): Observable<Resource> { 92 public saveResource(resource: Resource, config?: RequestConfig): Observable<Resource> {
74 return this.http.post<Resource>('/api/resource', resource, defaultHttpOptionsFromConfig(config)); 93 return this.http.post<Resource>('/api/resource', resource, defaultHttpOptionsFromConfig(config));
75 } 94 }
@@ -85,10 +85,27 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC @@ -85,10 +85,27 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
85 85
86 this.config.entitiesFetchFunction = pageLink => this.resourceService.getResources(pageLink) as Observable<PageData<Resource>>; 86 this.config.entitiesFetchFunction = pageLink => this.resourceService.getResources(pageLink) as Observable<PageData<Resource>>;
87 this.config.loadEntity = id => this.resourceService.getResource(id.id); 87 this.config.loadEntity = id => this.resourceService.getResource(id.id);
88 - this.config.saveEntity = resource => this.resourceService.saveResource(resource); 88 + this.config.saveEntity = resource => this.saveResource(resource);
89 this.config.deleteEntity = id => this.resourceService.deleteResource(id.id); 89 this.config.deleteEntity = id => this.resourceService.deleteResource(id.id);
90 } 90 }
91 91
  92 + saveResource(resource) {
  93 + if (Array.isArray(resource.data)) {
  94 + const resources = [];
  95 + resource.data.forEach((data, index) => {
  96 + resources.push({
  97 + resourceType: resource.resourceType,
  98 + data,
  99 + fileName: resource.fileName[index],
  100 + title: resource.title
  101 + });
  102 + });
  103 + return this.resourceService.saveResources(resources, {resendRequest: true});
  104 + } else {
  105 + return this.resourceService.saveResource(resource);
  106 + }
  107 + }
  108 +
92 resolve(): EntityTableConfig<Resource> { 109 resolve(): EntityTableConfig<Resource> {
93 this.config.tableTitle = this.translate.instant('resource.resources-library'); 110 this.config.tableTitle = this.translate.instant('resource.resources-library');
94 const authUser = getCurrentAuthUser(this.store); 111 const authUser = getCurrentAuthUser(this.store);
@@ -44,9 +44,11 @@ @@ -44,9 +44,11 @@
44 <tb-file-input 44 <tb-file-input
45 formControlName="data" 45 formControlName="data"
46 required 46 required
47 - [convertToBase64]="true" 47 + [readAsBinary]="true"
48 [allowedExtensions]="getAllowedExtensions()" 48 [allowedExtensions]="getAllowedExtensions()"
  49 + [contentConvertFunction]="convertToBase64File"
49 [accept]="getAcceptType()" 50 [accept]="getAcceptType()"
  51 + [multipleFile]="entityForm.get('resourceType').value === resourceType.LWM2M_MODEL"
50 dropLabel="{{'resource.drop-file' | translate}}" 52 dropLabel="{{'resource.drop-file' | translate}}"
51 [existingFileName]="entityForm.get('fileName')?.value" 53 [existingFileName]="entityForm.get('fileName')?.value"
52 (fileNameChanged)="entityForm?.get('fileName').patchValue($event)"> 54 (fileNameChanged)="entityForm?.get('fileName').patchValue($event)">
@@ -29,7 +29,7 @@ import { @@ -29,7 +29,7 @@ import {
29 ResourceTypeMIMETypes, 29 ResourceTypeMIMETypes,
30 ResourceTypeTranslationMap 30 ResourceTypeTranslationMap
31 } from '@shared/models/resource.models'; 31 } from '@shared/models/resource.models';
32 -import { distinctUntilChanged, takeUntil } from 'rxjs/operators'; 32 +import { pairwise, startWith, takeUntil } from 'rxjs/operators';
33 33
34 @Component({ 34 @Component({
35 selector: 'tb-resources-library', 35 selector: 'tb-resources-library',
@@ -54,15 +54,22 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme @@ -54,15 +54,22 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
54 ngOnInit() { 54 ngOnInit() {
55 super.ngOnInit(); 55 super.ngOnInit();
56 this.entityForm.get('resourceType').valueChanges.pipe( 56 this.entityForm.get('resourceType').valueChanges.pipe(
57 - distinctUntilChanged((oldValue, newValue) => [oldValue, newValue].includes(this.resourceType.LWM2M_MODEL)), 57 + startWith(ResourceType.LWM2M_MODEL),
  58 + pairwise(),
58 takeUntil(this.destroy$) 59 takeUntil(this.destroy$)
59 - ).subscribe((type) => { 60 + ).subscribe(([previousType, type]) => {
  61 + if (previousType === this.resourceType.LWM2M_MODEL) {
  62 + this.entityForm.get('title').setValidators(Validators.required);
  63 + this.entityForm.get('title').updateValueAndValidity({emitEvent: false});
  64 + }
60 if (type === this.resourceType.LWM2M_MODEL) { 65 if (type === this.resourceType.LWM2M_MODEL) {
61 this.entityForm.get('title').clearValidators(); 66 this.entityForm.get('title').clearValidators();
62 - } else {  
63 - this.entityForm.get('title').setValidators(Validators.required); 67 + this.entityForm.get('title').updateValueAndValidity({emitEvent: false});
64 } 68 }
65 - this.entityForm.get('title').updateValueAndValidity({emitEvent: false}); 69 + this.entityForm.patchValue({
  70 + data: null,
  71 + fileName: null
  72 + }, {emitEvent: false});
66 }); 73 });
67 } 74 }
68 75
@@ -119,4 +126,8 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme @@ -119,4 +126,8 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
119 return '*/*'; 126 return '*/*';
120 } 127 }
121 } 128 }
  129 +
  130 + convertToBase64File(data: string): string {
  131 + return window.btoa(data);
  132 + }
122 } 133 }
@@ -18,7 +18,7 @@ @@ -18,7 +18,7 @@
18 <div class="tb-container"> 18 <div class="tb-container">
19 <label class="tb-title">{{ label }}</label> 19 <label class="tb-title">{{ label }}</label>
20 <ng-container #flow="flow" 20 <ng-container #flow="flow"
21 - [flowConfig]="{singleFile: true, allowDuplicateUploads: true}"> 21 + [flowConfig]="{allowDuplicateUploads: true}">
22 <div class="tb-file-select-container"> 22 <div class="tb-file-select-container">
23 <div class="tb-file-clear-container"> 23 <div class="tb-file-clear-container">
24 <button mat-button mat-icon-button color="primary" 24 <button mat-button mat-icon-button color="primary"
@@ -34,7 +34,7 @@ @@ -34,7 +34,7 @@
34 flowDrop 34 flowDrop
35 [flow]="flow.flowJs"> 35 [flow]="flow.flowJs">
36 <label for="{{inputId}}">{{ dropLabel }}</label> 36 <label for="{{inputId}}">{{ dropLabel }}</label>
37 - <input class="file-input" flowButton type="file" [flow]="flow.flowJs" [flowAttributes]="{accept: accept}" id="{{inputId}}"> 37 + <input class="file-input" flowButton #flowInput type="file" [flow]="flow.flowJs" [flowAttributes]="{accept: accept}" id="{{inputId}}">
38 </div> 38 </div>
39 </div> 39 </div>
40 </ng-container> 40 </ng-container>
@@ -17,6 +17,7 @@ @@ -17,6 +17,7 @@
17 import { 17 import {
18 AfterViewInit, 18 AfterViewInit,
19 Component, 19 Component,
  20 + ElementRef,
20 EventEmitter, 21 EventEmitter,
21 forwardRef, 22 forwardRef,
22 Input, 23 Input,
@@ -102,17 +103,34 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, @@ -102,17 +103,34 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
102 existingFileName: string; 103 existingFileName: string;
103 104
104 @Input() 105 @Input()
105 - convertToBase64 = false; 106 + readAsBinary = false;
  107 +
  108 + private multipleFileValue = false;
  109 +
  110 + @Input()
  111 + set multipleFile(value: boolean) {
  112 + this.multipleFileValue = value;
  113 + if (this.flow?.flowJs) {
  114 + this.updateMultipleFileMode(this.multipleFile);
  115 + }
  116 + }
  117 +
  118 + get multipleFile(): boolean {
  119 + return this.multipleFileValue;
  120 + }
106 121
107 @Output() 122 @Output()
108 - fileNameChanged = new EventEmitter<string>(); 123 + fileNameChanged = new EventEmitter<string|string[]>();
109 124
110 - fileName: string; 125 + fileName: string | string[];
111 fileContent: any; 126 fileContent: any;
112 127
113 @ViewChild('flow', {static: true}) 128 @ViewChild('flow', {static: true})
114 flow: FlowDirective; 129 flow: FlowDirective;
115 130
  131 + @ViewChild('flowInput', {static: true})
  132 + flowInput: ElementRef;
  133 +
116 autoUploadSubscription: Subscription; 134 autoUploadSubscription: Subscription;
117 135
118 private propagateChange = null; 136 private propagateChange = null;
@@ -125,34 +143,60 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, @@ -125,34 +143,60 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
125 143
126 ngAfterViewInit() { 144 ngAfterViewInit() {
127 this.autoUploadSubscription = this.flow.events$.subscribe(event => { 145 this.autoUploadSubscription = this.flow.events$.subscribe(event => {
128 - if (event.type === 'fileAdded') {  
129 - const file = event.event[0] as flowjs.FlowFile;  
130 - if (this.filterFile(file)) {  
131 - const reader = new FileReader();  
132 - reader.onload = (loadEvent) => {  
133 - if (typeof reader.result === 'string') {  
134 - const fileContent = this.convertToBase64 ? window.btoa(reader.result) : reader.result;  
135 - if (fileContent && fileContent.length > 0) {  
136 - if (this.contentConvertFunction) {  
137 - this.fileContent = this.contentConvertFunction(fileContent);  
138 - } else {  
139 - this.fileContent = fileContent;  
140 - }  
141 - if (this.fileContent) {  
142 - this.fileName = file.name;  
143 - } else {  
144 - this.fileName = null;  
145 - }  
146 - this.updateModel();  
147 - } 146 + if (event.type === 'filesAdded') {
  147 + const readers = [];
  148 + (event.event[0] as flowjs.FlowFile[]).forEach(file => {
  149 + if (this.filterFile(file)) {
  150 + readers.push(this.readerAsFile(file));
  151 + }
  152 + });
  153 + if (readers.length) {
  154 + Promise.all(readers).then((filesContent) => {
  155 + filesContent = filesContent.filter(content => content.fileContent != null);
  156 + if (filesContent.length === 1) {
  157 + this.fileContent = filesContent[0].fileContent;
  158 + this.fileName = filesContent[0].fileName;
  159 + this.updateModel();
  160 + } else if (filesContent.length > 1) {
  161 + this.fileContent = filesContent.map(content => content.fileContent);
  162 + this.fileName = filesContent.map(content => content.fileName);
  163 + this.updateModel();
  164 + }
  165 + });
  166 + }
  167 + }
  168 + });
  169 + if (!this.multipleFile) {
  170 + this.updateMultipleFileMode(this.multipleFile);
  171 + }
  172 + }
  173 +
  174 + private readerAsFile(file: flowjs.FlowFile): Promise<any> {
  175 + return new Promise((resolve) => {
  176 + const reader = new FileReader();
  177 + reader.onload = () => {
  178 + let fileName = null;
  179 + let fileContent = null;
  180 + if (typeof reader.result === 'string') {
  181 + fileContent = reader.result;
  182 + if (fileContent && fileContent.length > 0) {
  183 + if (this.contentConvertFunction) {
  184 + fileContent = this.contentConvertFunction(fileContent);
  185 + }
  186 + if (fileContent) {
  187 + fileName = file.name;
148 } 188 }
149 - };  
150 - if (this.convertToBase64) {  
151 - reader.readAsBinaryString(file.file);  
152 - } else {  
153 - reader.readAsText(file.file);  
154 } 189 }
155 } 190 }
  191 + resolve({fileContent, fileName});
  192 + };
  193 + reader.onerror = () => {
  194 + resolve({fileContent: null, fileName: null});
  195 + };
  196 + if (this.readAsBinary) {
  197 + reader.readAsBinaryString(file.file);
  198 + } else {
  199 + reader.readAsText(file.file);
156 } 200 }
157 }); 201 });
158 } 202 }
@@ -207,4 +251,11 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, @@ -207,4 +251,11 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
207 this.fileContent = null; 251 this.fileContent = null;
208 this.updateModel(); 252 this.updateModel();
209 } 253 }
  254 +
  255 + private updateMultipleFileMode(multiple: boolean) {
  256 + this.flow.flowJs.opts.singleFile = !multiple;
  257 + if (!multiple) {
  258 + this.flowInput.nativeElement.removeAttribute('multiple');
  259 + }
  260 + }
210 } 261 }
@@ -59,3 +59,8 @@ export interface Resource extends ResourceInfo { @@ -59,3 +59,8 @@ export interface Resource extends ResourceInfo {
59 data: string; 59 data: string;
60 fileName: string; 60 fileName: string;
61 } 61 }
  62 +
  63 +export interface Resources extends ResourceInfo {
  64 + data: string|string[];
  65 + fileName: string|string[];
  66 +}