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);
TbResource image = new TbResource(imageInfo);
image.setData(file.getBytes());
image.setFileName(file.getOriginalFilename());
image.updateDescriptor(ImageDescriptor.class, descriptor -> {
descriptor.setMediaType(file.getContentType());
return descriptor;

View File

@ -58,7 +58,7 @@ export class ImageService {
}
const formData = new FormData();
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));
}

View File

@ -184,6 +184,7 @@ import {
import { ScrollGridComponent } from '@home/components/grid/scroll-grid.component';
import { ImageGalleryComponent } from '@home/components/image/image-gallery.component';
import { UploadImageDialogComponent } from '@home/components/image/upload-image-dialog.component';
import { ImageDialogComponent } from '@home/components/image/image-dialog.component';
@NgModule({
declarations:
@ -331,7 +332,8 @@ import { UploadImageDialogComponent } from '@home/components/image/upload-image-
SendNotificationButtonComponent,
ScrollGridComponent,
ImageGalleryComponent,
UploadImageDialogComponent
UploadImageDialogComponent,
ImageDialogComponent
],
imports: [
CommonModule,
@ -472,7 +474,8 @@ import { UploadImageDialogComponent } from '@home/components/image/upload-image-
SendNotificationButtonComponent,
ScrollGridComponent,
ImageGalleryComponent,
UploadImageDialogComponent
UploadImageDialogComponent,
ImageDialogComponent
],
providers: [
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>
<div fxFlex *ngIf="mode === 'list'" fxLayout="column">
<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>
<ng-container matColumnDef="select" sticky>
<mat-header-cell *matHeaderCellDef style="width: 30px;">
@ -185,7 +185,7 @@
(click)="editImage($event, image)">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button [disabled]="(isLoading$ | async) || !deleteEnabled(image)"
<button mat-icon-button [disabled]="(isLoading$ | async) || readonly(image)"
matTooltip="{{ 'image.delete-image' | translate }}"
matTooltipPosition="above"
(click)="deleteImage($event, image)">

View File

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

View File

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

View File

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

View File

@ -63,5 +63,8 @@
</button>
</div>
</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>

View File

@ -23,9 +23,12 @@ $previewSize: 78px !default;
.tb-container {
margin-top: 0;
padding: 0 0 16px;
display: flex;
flex-direction: column;
gap: 8px;
label.tb-title {
display: block;
padding-bottom: 8px;
padding-bottom: 0px;
}
}
@ -72,6 +75,30 @@ $previewSize: 78px !default;
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 {
display: none;
}

View File

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

View File

@ -25,6 +25,10 @@ const LOADING_IMAGE_DATA_URI = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS
'LTgiPz4KPHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZlcnNpb249IjEuMSIgdmlld0JveD0iMCAw' +
'IDIwIDIwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPgo=';
export interface UrlHolder {
url?: string;
}
@Pipe({
name: 'image'
})
@ -33,10 +37,11 @@ export class ImagePipe implements PipeTransform {
constructor(private imageService: ImageService,
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 asString = !!args?.asString;
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)) {
const preview = !!args?.preview;
this.imageService.resolveImageUrl(url, preview, asString).subscribe((imageUrl) => {

View File

@ -2958,6 +2958,7 @@
"import-image": "Import image",
"upload-image": "Upload image",
"edit-image": "Edit image",
"image-details": "Image details",
"no-images": "No images found",
"delete-image": "Delete image",
"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.",
"list-mode": "List view",
"grid-mode": "Grid view",
"image-preview": "Image preview"
"image-preview": "Image preview",
"update-image": "Update image"
},
"image-input": {
"drop-images-or": "Drag and drop an images or",