UI: Add resource file max size and improved file input style

This commit is contained in:
Vladyslav_Prykhodko 2023-12-22 15:13:18 +02:00
parent 58cf97f68a
commit 35deb47584
13 changed files with 109 additions and 43 deletions

View File

@ -25,6 +25,7 @@ export interface SysParamsState {
tbelEnabled: boolean;
persistDeviceStateToTelemetry: boolean;
userSettings: UserSettings;
maxResourceSize: number;
}
export interface SysParams extends SysParamsState {

View File

@ -29,6 +29,7 @@ const emptyUserAuthState: AuthPayload = {
hasRepository: false,
tbelEnabled: false,
persistDeviceStateToTelemetry: false,
maxResourceSize: 0,
userSettings: initialUserSettings
};

View File

@ -72,6 +72,11 @@ export const selectPersistDeviceStateToTelemetry = createSelector(
(state: AuthState) => state.persistDeviceStateToTelemetry
);
export const selectMaxResourceSize = createSelector(
selectAuthState,
(state: AuthState) => state.maxResourceSize
);
export const selectUserSettings = createSelector(
selectAuthState,
(state: AuthState) => state.userSettings

View File

@ -80,8 +80,7 @@
</mat-form-field>
</section>
<section [fxShow]="repositorySettingsForm.get('authMethod').value === repositoryAuthMethod.PRIVATE_KEY" fxLayout="column">
<tb-file-input style="margin-bottom: 16px;"
[existingFileName]="repositorySettingsForm.get('privateKeyFileName').value"
<tb-file-input [existingFileName]="repositorySettingsForm.get('privateKeyFileName').value"
required
formControlName="privateKey"
dropLabel="{{ 'admin.drop-private-key-file-or' | translate }}"

View File

@ -69,7 +69,9 @@
<tb-file-input *ngIf="isAdd || (isEdit && entityForm.get('resourceType').value === resourceType.JS_MODULE)"
formControlName="data"
required
label="{{ (entityForm.get('resourceType').value === resourceType.LWM2M_MODEL ? 'resource.resource-files' : 'resource.resource-file') | translate }}"
[readAsBinary]="true"
[maxSizeByte]="maxResourceSize"
[allowedExtensions]="getAllowedExtensions()"
[contentConvertFunction]="convertToBase64File"
[accept]="getAcceptType()"

View File

@ -16,7 +16,7 @@
import { ChangeDetectorRef, Component, Inject, OnDestroy, OnInit } from '@angular/core';
import { Subject } from 'rxjs';
import { Store } from '@ngrx/store';
import { select, Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { TranslateService } from '@ngx-translate/core';
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
@ -29,9 +29,10 @@ import {
ResourceTypeMIMETypes,
ResourceTypeTranslationMap
} from '@shared/models/resource.models';
import { filter, startWith, takeUntil } from 'rxjs/operators';
import { filter, startWith, take, takeUntil } from 'rxjs/operators';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { isDefinedAndNotNull } from '@core/utils';
import { selectMaxResourceSize } from '@core/auth/auth.selectors';
@Component({
selector: 'tb-resources-library',
@ -43,6 +44,8 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
readonly resourceTypes: ResourceType[] = Object.values(this.resourceType);
readonly resourceTypesTranslationMap = ResourceTypeTranslationMap;
maxResourceSize = 0;
private destroy$ = new Subject<void>();
constructor(protected store: Store<AppState>,
@ -52,6 +55,11 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
public fb: FormBuilder,
protected cd: ChangeDetectorRef) {
super(store, fb, entityValue, entitiesTableConfigValue, cd);
this.store.pipe(select(selectMaxResourceSize)).pipe(
take(1)
).subscribe(maxResourceSize => {
this.maxResourceSize = maxResourceSize;
});
}
ngOnInit() {

View File

@ -120,6 +120,7 @@
<tb-file-input
formControlName="file"
workFromFileObj="true"
label="{{ 'ota-update.package-file' | translate }}"
[required]="!entityForm.get('isURL').value"
dropLabel="{{'ota-update.drop-package-file-or' | translate}}">
</tb-file-input>

View File

@ -16,7 +16,7 @@
-->
<div class="tb-container">
<label class="tb-title"
<label class="tb-title" *ngIf="label"
[class.tb-required]="!disabled && required"
[class.pointer-event]="hint"
tb-hint-tooltip-icon="{{ hint }}">{{ label }}
@ -38,7 +38,7 @@
flowDrop
[flow]="flow.flowJs">
<div class="upload-label">
<mat-icon>cloud_upload</mat-icon>
<mat-icon class="tb-mat-32">cloud_upload</mat-icon>
<span>{{ dropLabel }}</span>
<button type="button" mat-button color="primary" class="browse-file">
<label
@ -50,9 +50,10 @@
</div>
</div>
</ng-container>
</div>
<div>
<tb-error *ngIf="!fileName && required && requiredAsError" error="{{ noFileText | translate }}"></tb-error>
<div *ngIf="!fileName && !requiredAsError" translate>{{ noFileText }}</div>
<div *ngIf="fileName">{{ fileName }}</div>
<div class="tb-file-info-container">
<tb-error *ngIf="!fileName && required && requiredAsError" class="tb-file-name" error="{{ noFileText | translate }}"></tb-error>
<div *ngIf="!fileName && !requiredAsError" class="tb-file-name" translate>{{ noFileText }}</div>
<div *ngIf="fileName" class="tb-file-name" [title]="fileName">{{ fileName }}</div>
<div *ngIf="maxSizeByte && !disabled" class="tb-file-hint" translate [translateParams]="{ size: maxSizeByte | fileSize }">dashboard.maximum-upload-file-size</div>
</div>
</div>

View File

@ -21,9 +21,13 @@ $previewSize: 100px !default;
.tb-container {
margin-top: 0;
padding: 0 0 16px;
display: flex;
flex-direction: column;
gap: 8px;
label.tb-title {
display: flex;
padding-bottom: 8px;
padding-bottom: 0;
}
}
@ -78,19 +82,46 @@ $previewSize: 100px !default;
text-align: center;
.mat-icon {
margin-right: 17px;
color: rgba(0,0,0,0.12);
}
}
}
.tb-file-info-container {
display: flex;
flex-direction: column;
gap: 8px;
font-size: 13px;
font-style: normal;
line-height: 16px;
letter-spacing: normal;
}
.tb-file-name {
color: rgba(0, 0, 0, 0.54);
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
}
.tb-file-hint {
color: rgba(0, 0, 0, 0.38);
font-weight: 400;
overflow: hidden;
text-overflow: ellipsis;
}
}
:host ::ng-deep {
button.browse-file {
button.mat-mdc-button.mat-mdc-button-base.browse-file {
padding: 0;
min-width: 0;
height: 24px;
font-size: 16px;
label {
display: block;
cursor: pointer;
padding: 0 16px;
padding: 0 8px;
}
}

View File

@ -32,10 +32,12 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Subscription } from 'rxjs';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { FlowDirective } from '@flowjs/ngx-flow';
import { TranslateService } from '@ngx-translate/core';
import { UtilsService } from '@core/services/utils.service';
import { DialogService } from '@core/services/dialog.service';
import { FileSizePipe } from '@shared/pipe/file-size.pipe';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-file-input',
@ -72,44 +74,29 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
@Input()
dropLabel: string;
@Input()
maxSizeByte: number;
@Input()
contentConvertFunction: (content: string) => any;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
@coerceBoolean()
required: boolean;
@Input()
set required(value: boolean) {
const newVal = coerceBooleanProperty(value);
if (this.requiredValue !== newVal) {
this.requiredValue = newVal;
}
}
private requiredAsErrorValue: boolean;
get requiredAsError(): boolean {
return this.requiredAsErrorValue;
}
@Input()
set requiredAsError(value: boolean) {
const newVal = coerceBooleanProperty(value);
if (this.requiredAsErrorValue !== newVal) {
this.requiredAsErrorValue = newVal;
}
}
@coerceBoolean()
requiredAsError: boolean;
@Input()
@coerceBoolean()
disabled: boolean;
@Input()
existingFileName: string;
@Input()
@coerceBoolean()
readAsBinary = false;
@Input()
@ -148,7 +135,9 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
constructor(protected store: Store<AppState>,
private utils: UtilsService,
public translate: TranslateService) {
private translate: TranslateService,
private dialog: DialogService,
private fileSize: FileSizePipe) {
super(store);
}
@ -156,11 +145,22 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
this.autoUploadSubscription = this.flow.events$.subscribe(event => {
if (event.type === 'filesAdded') {
const readers = [];
let showMaxSizeAlert = false;
(event.event[0] as flowjs.FlowFile[]).forEach(file => {
if (this.filterFile(file)) {
if (!this.checkMaxSize(file)) {
showMaxSizeAlert = true;
} else if (this.filterFile(file)) {
readers.push(this.readerAsFile(file));
}
});
if (showMaxSizeAlert) {
this.dialog.alert(
this.translate.instant('dashboard.cannot-upload-file'),
this.translate.instant('dashboard.maximum-upload-file-size', {size: this.fileSize.transform(this.maxSizeByte)})
).subscribe(() => { });
}
if (readers.length) {
Promise.all(readers).then((files) => {
files = files.filter(file => file.fileContent != null || file.files != null);
@ -218,6 +218,10 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
});
}
private checkMaxSize(file: flowjs.FlowFile): boolean {
return !(this.maxSizeByte && this.maxSizeByte < file.size);
}
private filterFile(file: flowjs.FlowFile): boolean {
if (this.allowedExtensions) {
return this.allowedExtensions.split(',').indexOf(file.getExtension()) > -1;

View File

@ -35,6 +35,7 @@
label="{{'image.image-preview' | translate}}"
formControlName="file"
showFileName
[maxSizeByte]="maxResourceSize"
[fileName]="data?.image?.fileName"
(fileNameChanged)="imageFileNameChanged($event)">
</tb-image-input>

View File

@ -17,7 +17,7 @@
import { Component, Inject, OnInit, SkipSelf } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { select, Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import {
FormGroupDirective,
@ -31,6 +31,8 @@ import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
import { ImageService } from '@core/http/image.service';
import { ImageResourceInfo, imageResourceType } from '@shared/models/resource.models';
import { selectMaxResourceSize } from '@core/auth/auth.selectors';
import { take } from 'rxjs/operators';
export interface UploadImageDialogData {
image?: ImageResourceInfo;
@ -51,6 +53,8 @@ export class UploadImageDialogComponent extends
submitted = false;
maxResourceSize = 0;
constructor(protected store: Store<AppState>,
protected router: Router,
private imageService: ImageService,
@ -59,6 +63,11 @@ export class UploadImageDialogComponent extends
public dialogRef: MatDialogRef<UploadImageDialogComponent, ImageResourceInfo>,
public fb: UntypedFormBuilder) {
super(store, router, dialogRef);
this.store.pipe(select(selectMaxResourceSize)).pipe(
take(1)
).subscribe(maxResourceSize => {
this.maxResourceSize = maxResourceSize;
});
}
ngOnInit(): void {

View File

@ -3511,6 +3511,7 @@
"ota-update": "OTA update",
"ota-update-details": "OTA update details",
"ota-updates": "OTA updates",
"package-file": "Package file",
"package-type": "Package type",
"packages-repository": "Packages repository",
"search": "Search packages",
@ -3697,6 +3698,8 @@
"no-resource-text": "No resources found",
"open-widgets-bundle": "Open widgets bundle",
"resource": "Resource",
"resource-file": "Resource file",
"resource-files": "Resource files",
"resource-library-details": "Resource details",
"resource-type": "Resource type",
"resources-library": "Resources library",