UI: Add resource file max size and improved file input style
This commit is contained in:
parent
58cf97f68a
commit
35deb47584
@ -25,6 +25,7 @@ export interface SysParamsState {
|
||||
tbelEnabled: boolean;
|
||||
persistDeviceStateToTelemetry: boolean;
|
||||
userSettings: UserSettings;
|
||||
maxResourceSize: number;
|
||||
}
|
||||
|
||||
export interface SysParams extends SysParamsState {
|
||||
|
||||
@ -29,6 +29,7 @@ const emptyUserAuthState: AuthPayload = {
|
||||
hasRepository: false,
|
||||
tbelEnabled: false,
|
||||
persistDeviceStateToTelemetry: false,
|
||||
maxResourceSize: 0,
|
||||
userSettings: initialUserSettings
|
||||
};
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 }}"
|
||||
|
||||
@ -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()"
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -35,6 +35,7 @@
|
||||
label="{{'image.image-preview' | translate}}"
|
||||
formControlName="file"
|
||||
showFileName
|
||||
[maxSizeByte]="maxResourceSize"
|
||||
[fileName]="data?.image?.fileName"
|
||||
(fileNameChanged)="imageFileNameChanged($event)">
|
||||
</tb-image-input>
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user