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);
|
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;
|
||||||
|
|||||||
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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>
|
</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)">
|
||||||
|
|||||||
@ -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') {
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) => {
|
||||||
|
|||||||
@ -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…
x
Reference in New Issue
Block a user