Commit 2e5bbe2032d446d695f0c702cd1b881a7dd3b5e7
Merge remote-tracking branch 'origin/feature/multiple-file-input' into YevhenBon…
…darenko-develop/3.3-firmware
Showing
7 changed files
with
145 additions
and
40 deletions
@@ -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 | +} |