UI: Implement image edit/update.
This commit is contained in:
parent
12f5c25698
commit
678ac15324
@ -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;
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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)">
|
||||
|
||||
@ -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') {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,34 +43,42 @@ 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) {
|
||||
if (this.uploadImage) {
|
||||
const titleControl = this.uploadImageFormGroup.get('title');
|
||||
if (!titleControl.value || !titleControl.touched) {
|
||||
titleControl.setValue(fileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean {
|
||||
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
|
||||
@ -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;
|
||||
if (this.uploadImage) {
|
||||
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);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user