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