Browse Source

UI: Implement image edit/update.

pull/9542/head
Igor Kulikov 3 years ago
parent
commit
678ac15324
  1. 1
      application/src/main/java/org/thingsboard/server/controller/ImageController.java
  2. 2
      ui-ngx/src/app/core/http/image.service.ts
  3. 7
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  4. 94
      ui-ngx/src/app/modules/home/components/image/image-dialog.component.html
  5. 83
      ui-ngx/src/app/modules/home/components/image/image-dialog.component.scss
  6. 147
      ui-ngx/src/app/modules/home/components/image/image-dialog.component.ts
  7. 4
      ui-ngx/src/app/modules/home/components/image/image-gallery.component.html
  8. 37
      ui-ngx/src/app/modules/home/components/image/image-gallery.component.ts
  9. 9
      ui-ngx/src/app/modules/home/components/image/upload-image-dialog.component.html
  10. 54
      ui-ngx/src/app/modules/home/components/image/upload-image-dialog.component.ts
  11. 5
      ui-ngx/src/app/shared/components/image-input.component.html
  12. 29
      ui-ngx/src/app/shared/components/image-input.component.scss
  13. 25
      ui-ngx/src/app/shared/components/image-input.component.ts
  14. 7
      ui-ngx/src/app/shared/pipe/image.pipe.ts
  15. 4
      ui-ngx/src/assets/locale/locale.constant-en_US.json

1
application/src/main/java/org/thingsboard/server/controller/ImageController.java

@ -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;

2
ui-ngx/src/app/core/http/image.service.ts

@ -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));
} }

7
ui-ngx/src/app/modules/home/components/home-components.module.ts

@ -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,

94
ui-ngx/src/app/modules/home/components/image/image-dialog.component.html

@ -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>

83
ui-ngx/src/app/modules/home/components/image/image-dialog.component.scss

@ -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);
}
}
}
}

147
ui-ngx/src/app/modules/home/components/image/image-dialog.component.ts

@ -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);
}
);
}
}

4
ui-ngx/src/app/modules/home/components/image/image-gallery.component.html

@ -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)">

37
ui-ngx/src/app/modules/home/components/image/image-gallery.component.ts

@ -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') {

9
ui-ngx/src/app/modules/home/components/image/upload-image-dialog.component.html

@ -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>

54
ui-ngx/src/app/modules/home/components/image/upload-image-dialog.component.ts

@ -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,32 +43,40 @@ 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) {
const titleControl = this.uploadImageFormGroup.get('title'); if (this.uploadImage) {
if (!titleControl.value || !titleControl.touched) { const titleControl = this.uploadImageFormGroup.get('title');
titleControl.setValue(fileName); if (!titleControl.value || !titleControl.touched) {
titleControl.setValue(fileName);
}
} }
} }
@ -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;
const title: string = this.uploadImageFormGroup.get('title').value; if (this.uploadImage) {
this.imageService.uploadImage(file, title).subscribe( const title: string = this.uploadImageFormGroup.get('title').value;
() => { this.imageService.uploadImage(file, title).subscribe(
this.dialogRef.close(true); (res) => {
} this.dialogRef.close(res);
); }
);
} else {
const image = this.data.image;
this.imageService.updateImage(imageResourceType(image), image.resourceKey, file).subscribe(
(res) => {
this.dialogRef.close(res);
}
);
}
} }
} }

5
ui-ngx/src/app/shared/components/image-input.component.html

@ -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>

29
ui-ngx/src/app/shared/components/image-input.component.scss

@ -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;
} }

25
ui-ngx/src/app/shared/components/image-input.component.ts

@ -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) {
this.safeImageUrl = this.sanitizer.bypassSecurityTrustUrl(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);
}
} else { } else {
this.safeImageUrl = null; this.safeImageUrl = null;
} }

7
ui-ngx/src/app/shared/pipe/image.pipe.ts

@ -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) => {

4
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -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",

Loading…
Cancel
Save