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.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,35 +143,61 @@ 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 (event.type === 'filesAdded') {
|
||||
const readers = [];
|
||||
(event.event[0] as flowjs.FlowFile[]).forEach(file => {
|
||||
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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
resolve({fileContent, fileName});
|
||||
};
|
||||
if (this.convertToBase64) {
|
||||
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