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 18 import { HttpClient } from '@angular/common/http';
19 19 import { PageLink } from '@shared/models/page/page-link';
20 20 import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils';
21   -import { Observable } from 'rxjs';
  21 +import { forkJoin, Observable, of } from 'rxjs';
22 22 import { PageData } from '@shared/models/page/page-data';
23 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 26 @Injectable({
27 27 providedIn: 'root'
... ... @@ -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 92 public saveResource(resource: Resource, config?: RequestConfig): Observable<Resource> {
74 93 return this.http.post<Resource>('/api/resource', resource, defaultHttpOptionsFromConfig(config));
75 94 }
... ...
... ... @@ -85,10 +85,27 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
85 85
86 86 this.config.entitiesFetchFunction = pageLink => this.resourceService.getResources(pageLink) as Observable<PageData<Resource>>;
87 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 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 109 resolve(): EntityTableConfig<Resource> {
93 110 this.config.tableTitle = this.translate.instant('resource.resources-library');
94 111 const authUser = getCurrentAuthUser(this.store);
... ...
... ... @@ -44,9 +44,11 @@
44 44 <tb-file-input
45 45 formControlName="data"
46 46 required
47   - [convertToBase64]="true"
  47 + [readAsBinary]="true"
48 48 [allowedExtensions]="getAllowedExtensions()"
  49 + [contentConvertFunction]="convertToBase64File"
49 50 [accept]="getAcceptType()"
  51 + [multipleFile]="entityForm.get('resourceType').value === resourceType.LWM2M_MODEL"
50 52 dropLabel="{{'resource.drop-file' | translate}}"
51 53 [existingFileName]="entityForm.get('fileName')?.value"
52 54 (fileNameChanged)="entityForm?.get('fileName').patchValue($event)">
... ...
... ... @@ -29,7 +29,7 @@ import {
29 29 ResourceTypeMIMETypes,
30 30 ResourceTypeTranslationMap
31 31 } from '@shared/models/resource.models';
32   -import { distinctUntilChanged, takeUntil } from 'rxjs/operators';
  32 +import { pairwise, startWith, takeUntil } from 'rxjs/operators';
33 33
34 34 @Component({
35 35 selector: 'tb-resources-library',
... ... @@ -54,15 +54,22 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
54 54 ngOnInit() {
55 55 super.ngOnInit();
56 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 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 65 if (type === this.resourceType.LWM2M_MODEL) {
61 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 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 18 <div class="tb-container">
19 19 <label class="tb-title">{{ label }}</label>
20 20 <ng-container #flow="flow"
21   - [flowConfig]="{singleFile: true, allowDuplicateUploads: true}">
  21 + [flowConfig]="{allowDuplicateUploads: true}">
22 22 <div class="tb-file-select-container">
23 23 <div class="tb-file-clear-container">
24 24 <button mat-button mat-icon-button color="primary"
... ... @@ -34,7 +34,7 @@
34 34 flowDrop
35 35 [flow]="flow.flowJs">
36 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 38 </div>
39 39 </div>
40 40 </ng-container>
... ...
... ... @@ -17,6 +17,7 @@
17 17 import {
18 18 AfterViewInit,
19 19 Component,
  20 + ElementRef,
20 21 EventEmitter,
21 22 forwardRef,
22 23 Input,
... ... @@ -102,17 +103,34 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
102 103 existingFileName: string;
103 104
104 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 122 @Output()
108   - fileNameChanged = new EventEmitter<string>();
  123 + fileNameChanged = new EventEmitter<string|string[]>();
109 124
110   - fileName: string;
  125 + fileName: string | string[];
111 126 fileContent: any;
112 127
113 128 @ViewChild('flow', {static: true})
114 129 flow: FlowDirective;
115 130
  131 + @ViewChild('flowInput', {static: true})
  132 + flowInput: ElementRef;
  133 +
116 134 autoUploadSubscription: Subscription;
117 135
118 136 private propagateChange = null;
... ... @@ -125,34 +143,60 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
125 143
126 144 ngAfterViewInit() {
127 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 251 this.fileContent = null;
208 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 59 data: string;
60 60 fileName: string;
61 61 }
  62 +
  63 +export interface Resources extends ResourceInfo {
  64 + data: string|string[];
  65 + fileName: string|string[];
  66 +}
... ...