UI: Implement image edit/update.

This commit is contained in:
Igor Kulikov 2023-11-21 14:08:52 +02:00
parent 12f5c25698
commit 678ac15324
15 changed files with 468 additions and 40 deletions

View File

@ -113,6 +113,7 @@ public class ImageController extends BaseController {
TbResourceInfo imageInfo = checkImageInfo(type, key, Operation.WRITE); TbResourceInfo imageInfo = checkImageInfo(type, key, Operation.WRITE);
TbResource image = new TbResource(imageInfo); TbResource image = new TbResource(imageInfo);
image.setData(file.getBytes()); image.setData(file.getBytes());
image.setFileName(file.getOriginalFilename());
image.updateDescriptor(ImageDescriptor.class, descriptor -> { image.updateDescriptor(ImageDescriptor.class, descriptor -> {
descriptor.setMediaType(file.getContentType()); descriptor.setMediaType(file.getContentType());
return descriptor; return descriptor;

View File

@ -58,7 +58,7 @@ export class ImageService {
} }
const formData = new FormData(); const formData = new FormData();
formData.append('file', file); formData.append('file', file);
return this.http.post<ImageResourceInfo>(`${IMAGES_URL_PREFIX}/${type}/${encodeURIComponent(key)}`, formData, return this.http.put<ImageResourceInfo>(`${IMAGES_URL_PREFIX}/${type}/${encodeURIComponent(key)}`, formData,
defaultHttpUploadOptions(config.ignoreLoading, config.ignoreErrors, config.resendRequest)); defaultHttpUploadOptions(config.ignoreLoading, config.ignoreErrors, config.resendRequest));
} }

View File

@ -184,6 +184,7 @@ import {
import { ScrollGridComponent } from '@home/components/grid/scroll-grid.component'; import { ScrollGridComponent } from '@home/components/grid/scroll-grid.component';
import { ImageGalleryComponent } from '@home/components/image/image-gallery.component'; import { ImageGalleryComponent } from '@home/components/image/image-gallery.component';
import { UploadImageDialogComponent } from '@home/components/image/upload-image-dialog.component'; import { UploadImageDialogComponent } from '@home/components/image/upload-image-dialog.component';
import { ImageDialogComponent } from '@home/components/image/image-dialog.component';
@NgModule({ @NgModule({
declarations: declarations:
@ -331,7 +332,8 @@ import { UploadImageDialogComponent } from '@home/components/image/upload-image-
SendNotificationButtonComponent, SendNotificationButtonComponent,
ScrollGridComponent, ScrollGridComponent,
ImageGalleryComponent, ImageGalleryComponent,
UploadImageDialogComponent UploadImageDialogComponent,
ImageDialogComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -472,7 +474,8 @@ import { UploadImageDialogComponent } from '@home/components/image/upload-image-
SendNotificationButtonComponent, SendNotificationButtonComponent,
ScrollGridComponent, ScrollGridComponent,
ImageGalleryComponent, ImageGalleryComponent,
UploadImageDialogComponent UploadImageDialogComponent,
ImageDialogComponent
], ],
providers: [ providers: [
WidgetComponentService, WidgetComponentService,

View File

@ -0,0 +1,94 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form [formGroup]="imageFormGroup" (ngSubmit)="save()" style="width: 560px;">
<mat-toolbar color="primary">
<h2>{{ (readonly ? 'image.image-details' : 'image.edit-image') | translate }}</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async">
<mat-form-field class="mat-block">
<mat-label translate>image.name</mat-label>
<input matInput formControlName="title" required>
<mat-error *ngIf="imageFormGroup.get('title').hasError('required')">
{{ 'image.name-required' | translate }}
</mat-error>
</mat-form-field>
<div class="tb-image-container tb-primary-fill">
<div class="tb-image-content">
<div class="tb-image-preview-title" translate>image.image-preview</div>
<div class="tb-image-preview-container">
<div class="tb-image-preview-spacer"></div>
<img class="tb-image-preview" [src]="imagePreviewData | image: {preview: true} | async">
</div>
<div class="tb-image-preview-details">
<div>{{ image.descriptor.width }}x{{ image.descriptor.height }}</div>
<mat-divider vertical></mat-divider>
<div>{{ image.descriptor.size | fileSize }}</div>
</div>
<div class="tb-image-actions">
<button type="button"
mat-icon-button [disabled]="isLoading$ | async"
matTooltip="{{ 'image.download-image' | translate }}"
matTooltipPosition="above"
(click)="downloadImage($event)">
<mat-icon>file_download</mat-icon>
</button>
<button type="button"
mat-icon-button [disabled]="isLoading$ | async"
matTooltip="{{ 'image.export-image' | translate }}"
matTooltipPosition="above"
(click)="exportImage($event)">
<tb-icon>mdi:file-export</tb-icon>
</button>
<button *ngIf="!readonly"
type="button"
mat-stroked-button
color="primary"
[disabled]="isLoading$ | async"
(click)="updateImage($event)">
{{ 'image.update-image' | translate }}
</button>
</div>
</div>
</div>
</fieldset>
</div>
<div mat-dialog-actions fxLayout="row" fxLayoutAlign="end center">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ (readonly ? 'action.close' : 'action.cancel') | translate }}
</button>
<button *ngIf="!readonly" mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || imageFormGroup.invalid
|| !imageFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
</div>
</form>

View File

@ -0,0 +1,83 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
.tb-image-container {
width: fit-content;
border-radius: 4px;
border: 1px solid rgba(0, 0, 0, 0.05);
padding: 12px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
.tb-image-content {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
.tb-image-preview-title {
color: rgba(0, 0, 0, 0.54);
font-size: 13px;
font-style: normal;
font-weight: 500;
line-height: 16px;
}
.tb-image-preview-container {
position: relative;
width: 100%;
height: 100%;
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 4px;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
.tb-image-preview-spacer {
margin-top: 100%;
}
.tb-image-preview {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: contain;
}
}
.tb-image-preview-details {
display: flex;
align-items: center;
gap: 8px;
color: rgba(0, 0, 0, 0.38);
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
.mat-divider.mat-divider-vertical {
height: 20px;
}
}
.tb-image-actions {
display: flex;
align-items: center;
gap: 8px;
color: rgba(0,0,0,0.54);
}
}
}
}

View File

@ -0,0 +1,147 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { ChangeDetectorRef, Component, Inject, OnInit, SkipSelf } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import {
FormGroupDirective,
NgForm,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
Validators
} from '@angular/forms';
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 {
UploadImageDialogComponent,
UploadImageDialogData
} from '@home/components/image/upload-image-dialog.component';
import { UrlHolder } from '@shared/pipe/image.pipe';
export interface ImageDialogData {
readonly: boolean;
image: ImageResourceInfo;
}
@Component({
selector: 'tb-image-dialog',
templateUrl: './image-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: ImageDialogComponent}],
styleUrls: ['./image-dialog.component.scss']
})
export class ImageDialogComponent extends
DialogComponent<ImageDialogComponent, boolean> implements OnInit, ErrorStateMatcher {
image: ImageResourceInfo;
readonly: boolean;
imageFormGroup: UntypedFormGroup;
submitted = false;
imageChanged = false;
imagePreviewData: UrlHolder;
constructor(protected store: Store<AppState>,
protected router: Router,
private imageService: ImageService,
private dialog: MatDialog,
@Inject(MAT_DIALOG_DATA) private data: ImageDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<ImageDialogComponent, boolean>,
public fb: UntypedFormBuilder) {
super(store, router, dialogRef);
this.image = data.image;
this.readonly = data.readonly;
this.imagePreviewData = {
url: this.image.link
};
}
ngOnInit(): void {
this.imageFormGroup = this.fb.group({
title: [this.image.title, [Validators.required]]
});
if (this.data.readonly) {
this.imageFormGroup.disable();
}
}
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;
}
cancel(): void {
this.dialogRef.close(this.imageChanged);
}
downloadImage($event) {
if ($event) {
$event.stopPropagation();
}
this.imageService.downloadImage(imageResourceType(this.image), this.image.resourceKey).subscribe();
}
exportImage($event) {
if ($event) {
$event.stopPropagation();
}
// TODO:
}
updateImage($event): void {
if ($event) {
$event.stopPropagation();
}
this.dialog.open<UploadImageDialogComponent, UploadImageDialogData,
ImageResourceInfo>(UploadImageDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
image: this.image
}
}).afterClosed().subscribe((result) => {
if (result) {
this.imageChanged = true;
this.image = result;
this.imagePreviewData = {
url: this.image.link
};
}
});
}
save(): void {
this.submitted = true;
const title: string = this.imageFormGroup.get('title').value;
const image = {...this.image, ...{title}};
this.imageService.updateImageInfo(image).subscribe(
() => {
this.dialogRef.close(true);
}
);
}
}

View File

@ -110,7 +110,7 @@
</mat-toolbar> </mat-toolbar>
<div fxFlex *ngIf="mode === 'list'" fxLayout="column"> <div fxFlex *ngIf="mode === 'list'" fxLayout="column">
<div fxFlex class="table-container"> <div fxFlex class="table-container">
<table mat-table [dataSource]="dataSource" [trackBy]="trackByEntityId" <table mat-table [dataSource]="dataSource" [trackBy]="trackByEntity"
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()" matSortDisableClear> matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()" matSortDisableClear>
<ng-container matColumnDef="select" sticky> <ng-container matColumnDef="select" sticky>
<mat-header-cell *matHeaderCellDef style="width: 30px;"> <mat-header-cell *matHeaderCellDef style="width: 30px;">
@ -185,7 +185,7 @@
(click)="editImage($event, image)"> (click)="editImage($event, image)">
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
</button> </button>
<button mat-icon-button [disabled]="(isLoading$ | async) || !deleteEnabled(image)" <button mat-icon-button [disabled]="(isLoading$ | async) || readonly(image)"
matTooltip="{{ 'image.delete-image' | translate }}" matTooltip="{{ 'image.delete-image' | translate }}"
matTooltipPosition="above" matTooltipPosition="above"
(click)="deleteImage($event, image)"> (click)="deleteImage($event, image)">

View File

@ -60,7 +60,11 @@ import {
} from '@home/components/dashboard-page/add-widget-dialog.component'; } from '@home/components/dashboard-page/add-widget-dialog.component';
import { Widget } from '@shared/models/widget.models'; import { Widget } from '@shared/models/widget.models';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { UploadImageDialogComponent } from '@home/components/image/upload-image-dialog.component'; import {
UploadImageDialogComponent,
UploadImageDialogData
} from '@home/components/image/upload-image-dialog.component';
import { ImageDialogComponent, ImageDialogData } from '@home/components/image/image-dialog.component';
@Component({ @Component({
selector: 'tb-image-gallery', selector: 'tb-image-gallery',
@ -350,16 +354,16 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe
this.textSearch.reset(); this.textSearch.reset();
} }
trackByEntityId(index: number, entity: BaseData<HasId>) { trackByEntity(index: number, entity: BaseData<HasId>) {
return entity.id.id; return entity;
} }
isSystem(image?: ImageResourceInfo): boolean { isSystem(image?: ImageResourceInfo): boolean {
return image?.tenantId?.id === NULL_UUID; return image?.tenantId?.id === NULL_UUID;
} }
deleteEnabled(image?: ImageResourceInfo): boolean { readonly(image?: ImageResourceInfo): boolean {
return this.authUser.authority === Authority.SYS_ADMIN || !this.isSystem(image); return this.authUser.authority !== Authority.SYS_ADMIN && this.isSystem(image);
} }
deleteImage($event: Event, image: ImageResourceInfo) { deleteImage($event: Event, image: ImageResourceInfo) {
@ -424,10 +428,11 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe
} }
uploadImage(): void { uploadImage(): void {
this.dialog.open<UploadImageDialogComponent, any, this.dialog.open<UploadImageDialogComponent, UploadImageDialogData,
boolean>(UploadImageDialogComponent, { ImageResourceInfo>(UploadImageDialogComponent, {
disableClose: true, disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {}
}).afterClosed().subscribe((result) => { }).afterClosed().subscribe((result) => {
if (result) { if (result) {
this.updateData(); this.updateData();
@ -435,11 +440,23 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe
}); });
} }
editImage($event, image: ImageResourceInfo) { editImage($event: Event, image: ImageResourceInfo) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
// TODO: this.dialog.open<ImageDialogComponent, ImageDialogData,
boolean>(ImageDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
image,
readonly: this.readonly(image)
}
}).afterClosed().subscribe((result) => {
if (result) {
this.updateData();
}
});
} }
protected updatedRouterParamsAndData(queryParams: object, queryParamsHandling: QueryParamsHandling = 'merge') { protected updatedRouterParamsAndData(queryParams: object, queryParamsHandling: QueryParamsHandling = 'merge') {

View File

@ -17,7 +17,7 @@
--> -->
<form [formGroup]="uploadImageFormGroup" (ngSubmit)="upload()" style="width: 560px;"> <form [formGroup]="uploadImageFormGroup" (ngSubmit)="upload()" style="width: 560px;">
<mat-toolbar color="primary"> <mat-toolbar color="primary">
<h2>{{ 'image.upload-image' | translate }}</h2> <h2>{{ ( uploadImage ? 'image.upload-image' : 'image.update-image' ) | translate }}</h2>
<span fxFlex></span> <span fxFlex></span>
<button mat-icon-button <button mat-icon-button
(click)="cancel()" (click)="cancel()"
@ -31,11 +31,14 @@
<div mat-dialog-content> <div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async"> <fieldset [disabled]="isLoading$ | async">
<tb-image-input resultAsFile <tb-image-input resultAsFile
processImageApiLink
label="{{'image.image-preview' | translate}}" label="{{'image.image-preview' | translate}}"
formControlName="file" formControlName="file"
showFileName
[fileName]="data?.image?.fileName"
(fileNameChanged)="imageFileNameChanged($event)"> (fileNameChanged)="imageFileNameChanged($event)">
</tb-image-input> </tb-image-input>
<mat-form-field *ngIf="uploadImageFormGroup.get('file').value" class="mat-block"> <mat-form-field *ngIf="uploadImage && uploadImageFormGroup.get('file').value" class="mat-block">
<mat-label translate>image.name</mat-label> <mat-label translate>image.name</mat-label>
<input matInput formControlName="title" required> <input matInput formControlName="title" required>
<mat-error *ngIf="uploadImageFormGroup.get('title').hasError('required')"> <mat-error *ngIf="uploadImageFormGroup.get('title').hasError('required')">
@ -55,7 +58,7 @@
type="submit" type="submit"
[disabled]="(isLoading$ | async) || uploadImageFormGroup.invalid [disabled]="(isLoading$ | async) || uploadImageFormGroup.invalid
|| !uploadImageFormGroup.dirty"> || !uploadImageFormGroup.dirty">
{{ 'action.upload' | translate }} {{ (uploadImage ? 'action.upload' : 'action.update') | translate }}
</button> </button>
</div> </div>
</form> </form>

View File

@ -14,9 +14,9 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, OnInit, SkipSelf } from '@angular/core'; import { Component, Inject, OnInit, SkipSelf } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core'; import { ErrorStateMatcher } from '@angular/material/core';
import { MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { import {
@ -30,6 +30,11 @@ import {
import { DialogComponent } from '@shared/components/dialog.component'; import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { ImageService } from '@core/http/image.service'; import { ImageService } from '@core/http/image.service';
import { ImageResourceInfo, imageResourceType } from '@shared/models/resource.models';
export interface UploadImageDialogData {
image?: ImageResourceInfo;
}
@Component({ @Component({
selector: 'tb-upload-image-dialog', selector: 'tb-upload-image-dialog',
@ -38,34 +43,42 @@ import { ImageService } from '@core/http/image.service';
styleUrls: [] styleUrls: []
}) })
export class UploadImageDialogComponent extends export class UploadImageDialogComponent extends
DialogComponent<UploadImageDialogComponent, boolean> implements OnInit, ErrorStateMatcher { DialogComponent<UploadImageDialogComponent, ImageResourceInfo> implements OnInit, ErrorStateMatcher {
uploadImageFormGroup: UntypedFormGroup; uploadImageFormGroup: UntypedFormGroup;
uploadImage = true;
submitted = false; submitted = false;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
private imageService: ImageService, private imageService: ImageService,
@Inject(MAT_DIALOG_DATA) public data: UploadImageDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, @SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<UploadImageDialogComponent, boolean>, public dialogRef: MatDialogRef<UploadImageDialogComponent, ImageResourceInfo>,
public fb: UntypedFormBuilder) { public fb: UntypedFormBuilder) {
super(store, router, dialogRef); super(store, router, dialogRef);
} }
ngOnInit(): void { ngOnInit(): void {
this.uploadImage = !this.data?.image;
this.uploadImageFormGroup = this.fb.group({ this.uploadImageFormGroup = this.fb.group({
file: [null, [Validators.required]], file: [this.data?.image?.link, [Validators.required]]
title: [null, [Validators.required]]
}); });
if (this.uploadImage) {
this.uploadImageFormGroup.addControl('title', this.fb.control(null, [Validators.required]));
}
} }
imageFileNameChanged(fileName: string) { imageFileNameChanged(fileName: string) {
if (this.uploadImage) {
const titleControl = this.uploadImageFormGroup.get('title'); const titleControl = this.uploadImageFormGroup.get('title');
if (!titleControl.value || !titleControl.touched) { if (!titleControl.value || !titleControl.touched) {
titleControl.setValue(fileName); titleControl.setValue(fileName);
} }
} }
}
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form); const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
@ -74,17 +87,26 @@ export class UploadImageDialogComponent extends
} }
cancel(): void { cancel(): void {
this.dialogRef.close(false); this.dialogRef.close(null);
} }
upload(): void { upload(): void {
this.submitted = true; this.submitted = true;
const file: File = this.uploadImageFormGroup.get('file').value; const file: File = this.uploadImageFormGroup.get('file').value;
if (this.uploadImage) {
const title: string = this.uploadImageFormGroup.get('title').value; const title: string = this.uploadImageFormGroup.get('title').value;
this.imageService.uploadImage(file, title).subscribe( this.imageService.uploadImage(file, title).subscribe(
() => { (res) => {
this.dialogRef.close(true); this.dialogRef.close(res);
}
);
} else {
const image = this.data.image;
this.imageService.updateImage(imageResourceType(image), image.resourceKey, file).subscribe(
(res) => {
this.dialogRef.close(res);
} }
); );
} }
}
} }

View File

@ -63,5 +63,8 @@
</button> </button>
</div> </div>
</ng-container> </ng-container>
<div class="tb-hint" *ngIf="maxSizeByte && !disabled" translate [translateParams]="{ size: maxSizeByte | fileSize}">dashboard.maximum-upload-file-size</div> <div *ngIf="(showFileName && fileName) || (maxSizeByte && !disabled)" class="tb-image-info-container">
<div *ngIf="showFileName && fileName" class="tb-image-file-name">{{ fileName }}</div>
<div *ngIf="maxSizeByte && !disabled" class="tb-image-file-hint" translate [translateParams]="{ size: maxSizeByte | fileSize}">dashboard.maximum-upload-file-size</div>
</div>
</div> </div>

View File

@ -23,9 +23,12 @@ $previewSize: 78px !default;
.tb-container { .tb-container {
margin-top: 0; margin-top: 0;
padding: 0 0 16px; padding: 0 0 16px;
display: flex;
flex-direction: column;
gap: 8px;
label.tb-title { label.tb-title {
display: block; display: block;
padding-bottom: 8px; padding-bottom: 0px;
} }
} }
@ -72,6 +75,30 @@ $previewSize: 78px !default;
max-height: $previewSize - 2px; max-height: $previewSize - 2px;
} }
.tb-image-info-container {
display: flex;
gap: 12px;
font-size: 13px;
font-style: normal;
line-height: 16px;
letter-spacing: normal;
}
.tb-image-file-name {
width: 136px;
color: rgba(0, 0, 0, 0.54);
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
}
.tb-image-file-hint {
color: rgba(0, 0, 0, 0.38);
font-weight: 400;
overflow: hidden;
text-overflow: ellipsis;
}
.file-input { .file-input {
display: none; display: none;
} }

View File

@ -37,6 +37,7 @@ import { DialogService } from '@core/services/dialog.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { FileSizePipe } from '@shared/pipe/file-size.pipe'; import { FileSizePipe } from '@shared/pipe/file-size.pipe';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { ImagePipe } from '@shared/pipe/image.pipe';
@Component({ @Component({
selector: 'tb-image-input', selector: 'tb-image-input',
@ -84,16 +85,27 @@ export class ImageInputComponent extends PageComponent implements AfterViewInit,
@Input() @Input()
inputId = this.utils.guid(); inputId = this.utils.guid();
@Input()
@coerceBoolean()
processImageApiLink = false;
@Input() @Input()
@coerceBoolean() @coerceBoolean()
resultAsFile = false; resultAsFile = false;
@Input()
@coerceBoolean()
showFileName = false;
@Input()
fileName: string;
@Output() @Output()
fileNameChanged = new EventEmitter<string>(); fileNameChanged = new EventEmitter<string>();
imageUrl: string; imageUrl: string;
file: File; file: File;
fileName: string;
safeImageUrl: SafeUrl; safeImageUrl: SafeUrl;
@ViewChild('flow', {static: true}) @ViewChild('flow', {static: true})
@ -106,6 +118,7 @@ export class ImageInputComponent extends PageComponent implements AfterViewInit,
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private utils: UtilsService, private utils: UtilsService,
private sanitizer: DomSanitizer, private sanitizer: DomSanitizer,
private imagePipe: ImagePipe,
private dialog: DialogService, private dialog: DialogService,
private translate: TranslateService, private translate: TranslateService,
private fileSize: FileSizePipe, private fileSize: FileSizePipe,
@ -161,7 +174,15 @@ export class ImageInputComponent extends PageComponent implements AfterViewInit,
writeValue(value: string): void { writeValue(value: string): void {
this.imageUrl = value; this.imageUrl = value;
if (this.imageUrl) { if (this.imageUrl) {
if (this.processImageApiLink) {
this.imagePipe.transform(this.imageUrl, {preview: true, ignoreLoadingImage: true}).subscribe(
(res) => {
this.safeImageUrl = res;
}
);
} else {
this.safeImageUrl = this.sanitizer.bypassSecurityTrustUrl(this.imageUrl); this.safeImageUrl = this.sanitizer.bypassSecurityTrustUrl(this.imageUrl);
}
} else { } else {
this.safeImageUrl = null; this.safeImageUrl = null;
} }

View File

@ -25,6 +25,10 @@ const LOADING_IMAGE_DATA_URI = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS
'LTgiPz4KPHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZlcnNpb249IjEuMSIgdmlld0JveD0iMCAw' + 'LTgiPz4KPHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZlcnNpb249IjEuMSIgdmlld0JveD0iMCAw' +
'IDIwIDIwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPgo='; 'IDIwIDIwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPgo=';
export interface UrlHolder {
url?: string;
}
@Pipe({ @Pipe({
name: 'image' name: 'image'
}) })
@ -33,10 +37,11 @@ export class ImagePipe implements PipeTransform {
constructor(private imageService: ImageService, constructor(private imageService: ImageService,
private sanitizer: DomSanitizer) { } private sanitizer: DomSanitizer) { }
transform(url: string, args?: any): Observable<SafeUrl | string> { transform(urlData: string | UrlHolder, args?: any): Observable<SafeUrl | string> {
const ignoreLoadingImage = !!args?.ignoreLoadingImage; const ignoreLoadingImage = !!args?.ignoreLoadingImage;
const asString = !!args?.asString; const asString = !!args?.asString;
const image$ = ignoreLoadingImage ? new Subject<SafeUrl | string>() : new BehaviorSubject<SafeUrl | string>(LOADING_IMAGE_DATA_URI); const image$ = ignoreLoadingImage ? new Subject<SafeUrl | string>() : new BehaviorSubject<SafeUrl | string>(LOADING_IMAGE_DATA_URI);
const url = (typeof urlData === 'string') ? urlData : urlData?.url;
if (isDefinedAndNotNull(url)) { if (isDefinedAndNotNull(url)) {
const preview = !!args?.preview; const preview = !!args?.preview;
this.imageService.resolveImageUrl(url, preview, asString).subscribe((imageUrl) => { this.imageService.resolveImageUrl(url, preview, asString).subscribe((imageUrl) => {

View File

@ -2958,6 +2958,7 @@
"import-image": "Import image", "import-image": "Import image",
"upload-image": "Upload image", "upload-image": "Upload image",
"edit-image": "Edit image", "edit-image": "Edit image",
"image-details": "Image details",
"no-images": "No images found", "no-images": "No images found",
"delete-image": "Delete image", "delete-image": "Delete image",
"delete-image-title": "Are you sure you want to delete image '{{imageTitle}}'?", "delete-image-title": "Are you sure you want to delete image '{{imageTitle}}'?",
@ -2966,7 +2967,8 @@
"delete-images-text": "Be careful, after the confirmation all selected images will be removed and all related data will become unrecoverable.", "delete-images-text": "Be careful, after the confirmation all selected images will be removed and all related data will become unrecoverable.",
"list-mode": "List view", "list-mode": "List view",
"grid-mode": "Grid view", "grid-mode": "Grid view",
"image-preview": "Image preview" "image-preview": "Image preview",
"update-image": "Update image"
}, },
"image-input": { "image-input": {
"drop-images-or": "Drag and drop an images or", "drop-images-or": "Drag and drop an images or",