UI: Image Gallery component.
This commit is contained in:
parent
52d2376931
commit
276debbd5d
@ -54,9 +54,6 @@ import org.thingsboard.server.service.security.model.SecurityUser;
|
|||||||
import org.thingsboard.server.service.security.permission.Operation;
|
import org.thingsboard.server.service.security.permission.Operation;
|
||||||
import org.thingsboard.server.service.security.permission.Resource;
|
import org.thingsboard.server.service.security.permission.Resource;
|
||||||
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
|
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
|
||||||
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
|
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
|
||||||
import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_SORT_PROPERTY_ALLOWABLE_VALUES;
|
import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_SORT_PROPERTY_ALLOWABLE_VALUES;
|
||||||
@ -80,14 +77,19 @@ public class ImageController extends BaseController {
|
|||||||
|
|
||||||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
|
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
|
||||||
@PostMapping("/api/image")
|
@PostMapping("/api/image")
|
||||||
public TbResourceInfo uploadImage(@RequestPart MultipartFile file) throws Exception {
|
public TbResourceInfo uploadImage(@RequestPart MultipartFile file,
|
||||||
|
@RequestPart(required = false) String title) throws Exception {
|
||||||
SecurityUser user = getCurrentUser();
|
SecurityUser user = getCurrentUser();
|
||||||
TbResource image = new TbResource();
|
TbResource image = new TbResource();
|
||||||
image.setTenantId(user.getTenantId());
|
image.setTenantId(user.getTenantId());
|
||||||
accessControlService.checkPermission(user, Resource.TB_RESOURCE, Operation.CREATE, null, image);
|
accessControlService.checkPermission(user, Resource.TB_RESOURCE, Operation.CREATE, null, image);
|
||||||
|
|
||||||
image.setFileName(file.getOriginalFilename());
|
image.setFileName(file.getOriginalFilename());
|
||||||
image.setTitle(file.getOriginalFilename());
|
if (StringUtils.isNotEmpty(title)) {
|
||||||
|
image.setTitle(title);
|
||||||
|
} else {
|
||||||
|
image.setTitle(file.getOriginalFilename());
|
||||||
|
}
|
||||||
image.setResourceType(ResourceType.IMAGE);
|
image.setResourceType(ResourceType.IMAGE);
|
||||||
ImageDescriptor descriptor = new ImageDescriptor();
|
ImageDescriptor descriptor = new ImageDescriptor();
|
||||||
descriptor.setMediaType(file.getContentType());
|
descriptor.setMediaType(file.getContentType());
|
||||||
|
|||||||
@ -18,11 +18,21 @@ package org.thingsboard.server.dao.util;
|
|||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.apache.batik.anim.dom.SAXSVGDocumentFactory;
|
||||||
|
import org.apache.batik.bridge.BridgeContext;
|
||||||
|
import org.apache.batik.bridge.DocumentLoader;
|
||||||
|
import org.apache.batik.bridge.GVTBuilder;
|
||||||
|
import org.apache.batik.bridge.UserAgent;
|
||||||
|
import org.apache.batik.bridge.UserAgentAdapter;
|
||||||
|
import org.apache.batik.gvt.GraphicsNode;
|
||||||
import org.apache.batik.transcoder.TranscoderInput;
|
import org.apache.batik.transcoder.TranscoderInput;
|
||||||
import org.apache.batik.transcoder.TranscoderOutput;
|
import org.apache.batik.transcoder.TranscoderOutput;
|
||||||
import org.apache.batik.transcoder.image.PNGTranscoder;
|
import org.apache.batik.transcoder.image.PNGTranscoder;
|
||||||
|
import org.apache.batik.util.XMLResourceDescriptor;
|
||||||
import org.springframework.util.MimeType;
|
import org.springframework.util.MimeType;
|
||||||
import org.springframework.util.MimeTypeUtils;
|
import org.springframework.util.MimeTypeUtils;
|
||||||
|
import org.thingsboard.server.common.data.StringUtils;
|
||||||
|
import org.w3c.dom.Document;
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
@ -52,7 +62,7 @@ public class ImageUtils {
|
|||||||
|
|
||||||
public static ProcessedImage processImage(byte[] data, String mediaType, int thumbnailMaxDimension) throws Exception {
|
public static ProcessedImage processImage(byte[] data, String mediaType, int thumbnailMaxDimension) throws Exception {
|
||||||
if (mediaTypeToFileExtension(mediaType).equals("svg")) {
|
if (mediaTypeToFileExtension(mediaType).equals("svg")) {
|
||||||
return processSvgImage(data, thumbnailMaxDimension);
|
return processSvgImage(data, mediaType, thumbnailMaxDimension);
|
||||||
}
|
}
|
||||||
|
|
||||||
BufferedImage bufferedImage = ImageIO.read(new ByteArrayInputStream(data));
|
BufferedImage bufferedImage = ImageIO.read(new ByteArrayInputStream(data));
|
||||||
@ -90,10 +100,45 @@ public class ImageUtils {
|
|||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ProcessedImage processSvgImage(byte[] data, int thumbnailMaxDimension) throws Exception {
|
public static ProcessedImage processSvgImage(byte[] data, String mediaType, int thumbnailMaxDimension) throws Exception {
|
||||||
|
SAXSVGDocumentFactory factory = new SAXSVGDocumentFactory(
|
||||||
|
XMLResourceDescriptor.getXMLParserClassName());
|
||||||
|
Document document = factory.createDocument(
|
||||||
|
null, new ByteArrayInputStream(data));
|
||||||
|
Integer width = null;
|
||||||
|
Integer height = null;
|
||||||
|
String strWidth = document.getDocumentElement().getAttribute("width");
|
||||||
|
String strHeight = document.getDocumentElement().getAttribute("height");
|
||||||
|
if (StringUtils.isNotEmpty(strWidth) && StringUtils.isNotEmpty(strHeight)) {
|
||||||
|
width = Integer.parseInt(strWidth);
|
||||||
|
height = Integer.parseInt(strHeight);
|
||||||
|
} else {
|
||||||
|
String viewBox = document.getDocumentElement().getAttribute("viewBox");
|
||||||
|
if (StringUtils.isNotEmpty(viewBox)) {
|
||||||
|
String[] viewBoxValues = viewBox.split(" ");
|
||||||
|
if (viewBoxValues.length > 3) {
|
||||||
|
width = Integer.parseInt(viewBoxValues[2]);
|
||||||
|
height = Integer.parseInt(viewBoxValues[3]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (width == null) {
|
||||||
|
UserAgent agent = new UserAgentAdapter();
|
||||||
|
DocumentLoader loader= new DocumentLoader(agent);
|
||||||
|
BridgeContext context = new BridgeContext(agent, loader);
|
||||||
|
context.setDynamic(true);
|
||||||
|
GVTBuilder builder= new GVTBuilder();
|
||||||
|
GraphicsNode root = builder.build(context, document);
|
||||||
|
var bounds = root.getPrimitiveBounds();
|
||||||
|
if (bounds != null) {
|
||||||
|
width = (int) bounds.getWidth();
|
||||||
|
height = (int) bounds.getHeight();
|
||||||
|
}
|
||||||
|
}
|
||||||
ProcessedImage image = new ProcessedImage();
|
ProcessedImage image = new ProcessedImage();
|
||||||
image.setWidth(0);
|
image.setMediaType(mediaType);
|
||||||
image.setHeight(0);
|
image.setWidth(width == null ? 0 : width);
|
||||||
|
image.setHeight(height == null ? 0 : height);
|
||||||
image.setData(data);
|
image.setData(data);
|
||||||
image.setSize(data.length);
|
image.setSize(data.length);
|
||||||
|
|
||||||
|
|||||||
@ -41,12 +41,13 @@ export class ImageService {
|
|||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public uploadImage(file: File, config?: RequestConfig): Observable<ImageResourceInfo> {
|
public uploadImage(file: File, title: string, config?: RequestConfig): Observable<ImageResourceInfo> {
|
||||||
if (!config) {
|
if (!config) {
|
||||||
config = {};
|
config = {};
|
||||||
}
|
}
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
|
formData.append('title', title);
|
||||||
return this.http.post<ImageResourceInfo>('/api/image', formData,
|
return this.http.post<ImageResourceInfo>('/api/image', formData,
|
||||||
defaultHttpUploadOptions(config.ignoreLoading, config.ignoreErrors, config.resendRequest));
|
defaultHttpUploadOptions(config.ignoreLoading, config.ignoreErrors, config.resendRequest));
|
||||||
}
|
}
|
||||||
@ -133,7 +134,7 @@ export class ImageService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public deleteImage(type: ImageResourceType, key: string, config?: RequestConfig) {
|
public deleteImage(type: ImageResourceType, key: string, config?: RequestConfig) {
|
||||||
return this.http.delete(`${IMAGES_URL_PREFIX}${type}/${encodeURIComponent}`, defaultHttpOptionsFromConfig(config));
|
return this.http.delete(`${IMAGES_URL_PREFIX}/${type}/${encodeURIComponent(key)}`, defaultHttpOptionsFromConfig(config));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -143,6 +143,13 @@ export class MenuService {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'images',
|
||||||
|
name: 'image.gallery',
|
||||||
|
type: 'link',
|
||||||
|
path: '/resources/images',
|
||||||
|
icon: 'filter'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'resources_library',
|
id: 'resources_library',
|
||||||
name: 'resource.resources-library',
|
name: 'resource.resources-library',
|
||||||
|
|||||||
@ -108,4 +108,8 @@ export class ScrollGridComponent<T, F> implements OnInit, AfterViewInit, OnChang
|
|||||||
trackByItem(index: number, item: T): T {
|
trackByItem(index: number, item: T): T {
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public update() {
|
||||||
|
this.dataSource.update();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -182,6 +182,8 @@ import {
|
|||||||
ExportWidgetsBundleDialogComponent
|
ExportWidgetsBundleDialogComponent
|
||||||
} from '@home/components/import-export/export-widgets-bundle-dialog.component';
|
} from '@home/components/import-export/export-widgets-bundle-dialog.component';
|
||||||
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 { UploadImageDialogComponent } from '@home/components/image/upload-image-dialog.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations:
|
declarations:
|
||||||
@ -327,7 +329,9 @@ import { ScrollGridComponent } from '@home/components/grid/scroll-grid.component
|
|||||||
RateLimitsTextComponent,
|
RateLimitsTextComponent,
|
||||||
RateLimitsDetailsDialogComponent,
|
RateLimitsDetailsDialogComponent,
|
||||||
SendNotificationButtonComponent,
|
SendNotificationButtonComponent,
|
||||||
ScrollGridComponent
|
ScrollGridComponent,
|
||||||
|
ImageGalleryComponent,
|
||||||
|
UploadImageDialogComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@ -466,7 +470,9 @@ import { ScrollGridComponent } from '@home/components/grid/scroll-grid.component
|
|||||||
RateLimitsTextComponent,
|
RateLimitsTextComponent,
|
||||||
RateLimitsDetailsDialogComponent,
|
RateLimitsDetailsDialogComponent,
|
||||||
SendNotificationButtonComponent,
|
SendNotificationButtonComponent,
|
||||||
ScrollGridComponent
|
ScrollGridComponent,
|
||||||
|
ImageGalleryComponent,
|
||||||
|
UploadImageDialogComponent
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
WidgetComponentService,
|
WidgetComponentService,
|
||||||
|
|||||||
@ -0,0 +1,269 @@
|
|||||||
|
<!--
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
-->
|
||||||
|
<div class="mat-padding tb-images tb-absolute-fill" [ngClass]="{'mat-padding': pageMode}">
|
||||||
|
<div fxFlex fxLayout="column" class="tb-images-content" [ngClass]="{'tb-outlined-border': pageMode}">
|
||||||
|
<mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="!textSearchMode && dataSource.selection.isEmpty()">
|
||||||
|
<div class="mat-toolbar-tools">
|
||||||
|
<span class="tb-images-title" translate>image.gallery</span>
|
||||||
|
<div class="tb-images-view-type-toolbar"
|
||||||
|
fxLayout="row"
|
||||||
|
fxLayoutAlign="start center">
|
||||||
|
<div class="tb-toolbar-button" [ngClass]="{'tb-selected' : mode === 'list'}">
|
||||||
|
<button mat-icon-button
|
||||||
|
(click)="setMode('list')"
|
||||||
|
matTooltip="{{'image.list-mode' | translate }}"
|
||||||
|
matTooltipPosition="below">
|
||||||
|
<mat-icon>view_list</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="tb-toolbar-button" [ngClass]="{'tb-selected' : mode === 'grid'}">
|
||||||
|
<button mat-icon-button
|
||||||
|
(click)="setMode('grid');"
|
||||||
|
matTooltip="{{'image.grid-mode' | translate }}"
|
||||||
|
matTooltipPosition="below">
|
||||||
|
<tb-icon>mdi:view-grid</tb-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<section fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
|
||||||
|
<span fxFlex></span>
|
||||||
|
<section fxLayout="row">
|
||||||
|
<button [disabled]="isLoading$ | async"
|
||||||
|
mat-icon-button
|
||||||
|
(click)="enterFilterMode()"
|
||||||
|
matTooltip="{{'action.search' | translate }}"
|
||||||
|
matTooltipPosition="above">
|
||||||
|
<mat-icon>search</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button [disabled]="isLoading$ | async"
|
||||||
|
mat-icon-button
|
||||||
|
(click)="updateData()"
|
||||||
|
matTooltip="{{'action.refresh' | translate }}"
|
||||||
|
matTooltipPosition="above">
|
||||||
|
<mat-icon>refresh</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button [disabled]="isLoading$ | async"
|
||||||
|
mat-icon-button
|
||||||
|
(click)="importImage()"
|
||||||
|
matTooltip="{{'image.import-image' | translate }}"
|
||||||
|
matTooltipPosition="above">
|
||||||
|
<mat-icon>upload</mat-icon>
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
<button [disabled]="isLoading$ | async"
|
||||||
|
mat-flat-button
|
||||||
|
color="primary"
|
||||||
|
(click)="uploadImage()">
|
||||||
|
{{ 'image.upload-image' | translate }}
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
</mat-toolbar>
|
||||||
|
<mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="textSearchMode && dataSource.selection.isEmpty()">
|
||||||
|
<div class="mat-toolbar-tools">
|
||||||
|
<button mat-icon-button
|
||||||
|
matTooltip="{{ 'image.search' | translate }}"
|
||||||
|
matTooltipPosition="above">
|
||||||
|
<mat-icon>search</mat-icon>
|
||||||
|
</button>
|
||||||
|
<mat-form-field fxFlex>
|
||||||
|
<mat-label> </mat-label>
|
||||||
|
<input #searchInput matInput
|
||||||
|
[formControl]="textSearch"
|
||||||
|
placeholder="{{ 'image.search' | translate }}"/>
|
||||||
|
</mat-form-field>
|
||||||
|
<button mat-icon-button (click)="exitFilterMode()"
|
||||||
|
matTooltip="{{ 'action.close' | translate }}"
|
||||||
|
matTooltipPosition="above">
|
||||||
|
<mat-icon>close</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</mat-toolbar>
|
||||||
|
<mat-toolbar class="mat-mdc-table-toolbar" color="primary" [fxShow]="!dataSource.selection.isEmpty()">
|
||||||
|
<div class="mat-toolbar-tools">
|
||||||
|
<span>
|
||||||
|
{{ translate.get('image.selected-images', {count: dataSource.selection.selected.length}) | async }}
|
||||||
|
</span>
|
||||||
|
<span fxFlex></span>
|
||||||
|
<button mat-icon-button [disabled]="isLoading$ | async"
|
||||||
|
matTooltip="{{ 'action.delete' | translate }}"
|
||||||
|
matTooltipPosition="above"
|
||||||
|
(click)="deleteImages($event)">
|
||||||
|
<mat-icon>delete</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</mat-toolbar>
|
||||||
|
<div fxFlex *ngIf="mode === 'list'" fxLayout="column">
|
||||||
|
<div fxFlex class="table-container">
|
||||||
|
<table mat-table [dataSource]="dataSource" [trackBy]="trackByEntityId"
|
||||||
|
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()" matSortDisableClear>
|
||||||
|
<ng-container matColumnDef="select" sticky>
|
||||||
|
<mat-header-cell *matHeaderCellDef style="width: 30px;">
|
||||||
|
<mat-checkbox (change)="$event ? dataSource.masterToggle() : null"
|
||||||
|
[checked]="dataSource.selection.hasValue() && (dataSource.isAllSelected() | async)"
|
||||||
|
[indeterminate]="dataSource.selection.hasValue() && (dataSource.isAllSelected() | async) === false">
|
||||||
|
</mat-checkbox>
|
||||||
|
</mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let image">
|
||||||
|
<mat-checkbox (click)="$event.stopPropagation()"
|
||||||
|
(change)="$event ? dataSource.selection.toggle(image) : null"
|
||||||
|
[checked]="dataSource.selection.isSelected(image)">
|
||||||
|
</mat-checkbox>
|
||||||
|
</mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container matColumnDef="preview">
|
||||||
|
<mat-header-cell *matHeaderCellDef style="width: 50px; min-width: 50px;"></mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let image">
|
||||||
|
<img class="tb-image-preview" [src]="image.link | image: {preview: true} | async" alt="{{ image.title }}">
|
||||||
|
</mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container matColumnDef="title">
|
||||||
|
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 100%;"> {{ 'image.name' | translate }} </mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let image">
|
||||||
|
{{ image.title }}
|
||||||
|
</mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container matColumnDef="createdTime">
|
||||||
|
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 200px; min-width: 200px;"> {{ 'image.created-time' | translate }} </mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let image">
|
||||||
|
{{ image.createdTime | date:'yyyy-MM-dd HH:mm:ss' }}
|
||||||
|
</mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container matColumnDef="resolution">
|
||||||
|
<mat-header-cell *matHeaderCellDef style="width: 160px; min-width: 160px;"> {{ 'image.resolution' | translate }} </mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let image">
|
||||||
|
{{ image.descriptor.width }}x{{ image.descriptor.height }}
|
||||||
|
</mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container matColumnDef="size">
|
||||||
|
<mat-header-cell *matHeaderCellDef style="width: 160px; min-width: 160px;"> {{ 'image.size' | translate }} </mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let image">
|
||||||
|
{{ image.descriptor.size | fileSize }}
|
||||||
|
</mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container matColumnDef="system">
|
||||||
|
<mat-header-cell *matHeaderCellDef style="width: 60px; min-width: 60px;"> {{ 'image.system' | translate }} </mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let image">
|
||||||
|
<mat-icon class="material-icons mat-icon">{{isSystem(image) ? 'check_box' : 'check_box_outline_blank'}}</mat-icon>
|
||||||
|
</mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container matColumnDef="actions" stickyEnd>
|
||||||
|
<mat-header-cell *matHeaderCellDef style="min-width: 192px; max-width: 192px; width: 192px">
|
||||||
|
</mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let image">
|
||||||
|
<div fxFlex fxLayout="row" fxLayoutAlign="end">
|
||||||
|
<button mat-icon-button [disabled]="isLoading$ | async"
|
||||||
|
matTooltip="{{ 'image.download-image' | translate }}"
|
||||||
|
matTooltipPosition="above"
|
||||||
|
(click)="downloadImage($event, image)">
|
||||||
|
<mat-icon>file_download</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button mat-icon-button [disabled]="isLoading$ | async"
|
||||||
|
matTooltip="{{ 'image.export-image' | translate }}"
|
||||||
|
matTooltipPosition="above"
|
||||||
|
(click)="exportImage($event, image)">
|
||||||
|
<tb-icon>mdi:file-export</tb-icon>
|
||||||
|
</button>
|
||||||
|
<button mat-icon-button [disabled]="isLoading$ | async"
|
||||||
|
matTooltip="{{ 'image.edit-image' | translate }}"
|
||||||
|
matTooltipPosition="above"
|
||||||
|
(click)="editImage($event, image)">
|
||||||
|
<mat-icon>edit</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button mat-icon-button [disabled]="(isLoading$ | async) || !deleteEnabled(image)"
|
||||||
|
matTooltip="{{ 'image.delete-image' | translate }}"
|
||||||
|
matTooltipPosition="above"
|
||||||
|
(click)="deleteImage($event, image)">
|
||||||
|
<mat-icon>delete</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
<mat-header-row [ngClass]="{'mat-row-select': true}" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
|
||||||
|
<mat-row [ngClass]="{'mat-row-select': true,
|
||||||
|
'mat-selected': dataSource.selection.isSelected(image)}"
|
||||||
|
*matRowDef="let image; columns: displayedColumns;" (click)="dataSource.selection.toggle(image)"></mat-row>
|
||||||
|
</table>
|
||||||
|
<ng-container *ngIf="(dataSource.isEmpty() | async) && !dataSource.dataLoading">
|
||||||
|
<ng-container *ngTemplateOutlet="noImages"></ng-container>
|
||||||
|
</ng-container>
|
||||||
|
<span [fxShow]="dataSource.dataLoading"
|
||||||
|
fxLayoutAlign="center center"
|
||||||
|
class="no-data-found">{{ 'common.loading' | translate }}</span>
|
||||||
|
</div>
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
<mat-paginator [length]="dataSource.total() | async"
|
||||||
|
[pageIndex]="pageLink.page"
|
||||||
|
[pageSize]="pageLink.pageSize"
|
||||||
|
[pageSizeOptions]="pageSizeOptions"
|
||||||
|
[hidePageSize]="hidePageSize"
|
||||||
|
showFirstLastButtons></mat-paginator>
|
||||||
|
</div>
|
||||||
|
<div fxFlex class="mat-padding" *ngIf="mode === 'grid'" fxLayout="column">
|
||||||
|
<tb-scroll-grid fxFlex
|
||||||
|
[columns]="gridColumns"
|
||||||
|
[itemSize]="150"
|
||||||
|
[fetchFunction]="gridImagesFetchFunction"
|
||||||
|
[filter]="gridImagesFilter"
|
||||||
|
[itemCard]="imageCard"
|
||||||
|
[loadingCell]="imageLoadingCard"
|
||||||
|
[dataLoading]="loadingImages"
|
||||||
|
[noData]="noImages">
|
||||||
|
</tb-scroll-grid>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ng-template #loadingImages>
|
||||||
|
<div fxLayout="column"
|
||||||
|
fxLayoutAlign="center center" class="tb-absolute-fill">
|
||||||
|
<span class="mat-headline-5" style="padding-bottom: 20px;">
|
||||||
|
{{ 'common.loading' | translate }}
|
||||||
|
</span>
|
||||||
|
<mat-spinner color="accent" strokeWidth="5"></mat-spinner>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template #noImages>
|
||||||
|
<div class="tb-no-images">
|
||||||
|
<div class="tb-no-data-bg"></div>
|
||||||
|
<div class="tb-no-data-text" translate>image.no-images</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template #imageCard let-item="item">
|
||||||
|
<div class="tb-image-card">
|
||||||
|
<div class="tb-image-preview-container">
|
||||||
|
<div class="tb-image-preview-spacer"></div>
|
||||||
|
<img class="tb-image-preview" [src]="item.link | image: {preview: true} | async" alt="{{ item.title }}">
|
||||||
|
</div>
|
||||||
|
<div class="tb-image-details">
|
||||||
|
<div class="tb-image-title-container">
|
||||||
|
<div class="tb-image-title">
|
||||||
|
{{ item.title }}
|
||||||
|
</div>
|
||||||
|
<div *ngIf="isSystem(item)" class="tb-image-sys">sys</div>
|
||||||
|
</div>
|
||||||
|
<div class="tb-image-info-container">
|
||||||
|
<div>{{ item.descriptor.width }}x{{ item.descriptor.height }}</div>
|
||||||
|
<mat-divider vertical></mat-divider>
|
||||||
|
<div>{{ item.descriptor.size | fileSize }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template #imageLoadingCard>
|
||||||
|
<div> TODO </div>
|
||||||
|
</ng-template>
|
||||||
@ -0,0 +1,182 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
$tb-button-selected-color: rgb(255, 110, 64) !default;
|
||||||
|
|
||||||
|
.tb-images {
|
||||||
|
.tb-images-content {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: #fff;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&.tb-outlined-border {
|
||||||
|
box-shadow: 0 0 0 0 rgb(0 0 0 / 20%), 0 0 0 0 rgb(0 0 0 / 14%), 0 0 0 0 rgb(0 0 0 / 12%);
|
||||||
|
border: solid 1px #e0e0e0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tb-images-title {
|
||||||
|
padding-right: 20px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tb-images-view-type-toolbar {
|
||||||
|
height: 55px;
|
||||||
|
min-height: 55px;
|
||||||
|
padding-right: 16px;
|
||||||
|
background-color: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
|
||||||
|
.tb-toolbar-button {
|
||||||
|
height: 48px;
|
||||||
|
button.mat-mdc-icon-button {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
&.tb-selected {
|
||||||
|
background-color: rgba(255, 255, 255, .15);
|
||||||
|
border-bottom: $tb-button-selected-color solid 4px;
|
||||||
|
|
||||||
|
button.mat-mdc-icon-button {
|
||||||
|
margin-bottom: -4px;
|
||||||
|
|
||||||
|
.mat-icon {
|
||||||
|
color: $tb-button-selected-color;
|
||||||
|
fill: $tb-button-selected-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container, tb-scroll-grid {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tb-no-images {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tb-image-card {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-direction: column;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: box-shadow 0.2s;
|
||||||
|
|
||||||
|
.tb-image-preview-container {
|
||||||
|
position: relative;
|
||||||
|
.tb-image-preview-spacer {
|
||||||
|
margin-top: 100%;
|
||||||
|
}
|
||||||
|
.tb-image-preview {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tb-image-details {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 4px;
|
||||||
|
align-self: stretch;
|
||||||
|
.tb-image-title-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
align-self: stretch;
|
||||||
|
.tb-image-title {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
color: rgba(0, 0, 0, 0.76);
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
.tb-image-sys {
|
||||||
|
padding: 1px 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba(236, 236, 236, 0.64);
|
||||||
|
color: rgba(0, 0, 0, 0.54);
|
||||||
|
font-size: 11px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 16px;
|
||||||
|
letter-spacing: 0.017px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tb-image-info-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
align-self: stretch;
|
||||||
|
color: rgba(0, 0, 0, 0.38);
|
||||||
|
font-size: 10px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 16px;
|
||||||
|
.mat-divider.mat-divider-vertical {
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 0 28px 0 rgba(0, 0, 0, 0.24);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container {
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
.mat-sort-header-sorted .mat-sort-header-arrow {
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
.mat-mdc-cell.mat-column-preview {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
padding: 2px 12px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tb-image-preview {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,545 @@
|
|||||||
|
///
|
||||||
|
/// 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 { CollectionViewer, DataSource, SelectionModel } from '@angular/cdk/collections';
|
||||||
|
import { ImageResourceInfo, imageResourceType } from '@shared/models/resource.models';
|
||||||
|
import { BehaviorSubject, forkJoin, merge, Observable, of, ReplaySubject, Subject, Subscription } from 'rxjs';
|
||||||
|
import { emptyPageData, PageData } from '@shared/models/page/page-data';
|
||||||
|
import { ImageService } from '@core/http/image.service';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { PageLink, PageQueryParam } from '@shared/models/page/page-link';
|
||||||
|
import { catchError, debounceTime, distinctUntilChanged, map, skip, takeUntil, tap } from 'rxjs/operators';
|
||||||
|
import {
|
||||||
|
AfterViewInit,
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Component,
|
||||||
|
ElementRef,
|
||||||
|
Input,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
ViewChild,
|
||||||
|
ViewEncapsulation
|
||||||
|
} from '@angular/core';
|
||||||
|
import { PageComponent } from '@shared/components/page.component';
|
||||||
|
import { MatPaginator } from '@angular/material/paginator';
|
||||||
|
import { MatSort, SortDirection } from '@angular/material/sort';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { AppState } from '@core/core.state';
|
||||||
|
import { UtilsService } from '@core/services/utils.service';
|
||||||
|
import { DialogService } from '@core/services/dialog.service';
|
||||||
|
import { FormBuilder } from '@angular/forms';
|
||||||
|
import { Direction, SortOrder } from '@shared/models/page/sort-order';
|
||||||
|
import { ResizeObserver } from '@juggle/resize-observer';
|
||||||
|
import { hidePageSizePixelValue } from '@shared/models/constants';
|
||||||
|
import { coerceBoolean } from '@shared/decorators/coercion';
|
||||||
|
import { ActivatedRoute, QueryParamsHandling, Router } from '@angular/router';
|
||||||
|
import { isEqual, isNotEmptyStr } from '@core/utils';
|
||||||
|
import { BaseData, HasId } from '@shared/models/base-data';
|
||||||
|
import { NULL_UUID } from '@shared/models/id/has-uuid';
|
||||||
|
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
|
||||||
|
import { Authority } from '@shared/models/authority.enum';
|
||||||
|
import { GridEntitiesFetchFunction, ScrollGridColumns } from '@home/models/datasource/scroll-grid-datasource';
|
||||||
|
import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
|
||||||
|
import { ScrollGridComponent } from '@home/components/grid/scroll-grid.component';
|
||||||
|
import {
|
||||||
|
AddWidgetDialogComponent,
|
||||||
|
AddWidgetDialogData
|
||||||
|
} 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';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'tb-image-gallery',
|
||||||
|
templateUrl: './image-gallery.component.html',
|
||||||
|
styleUrls: ['./image-gallery.component.scss'],
|
||||||
|
encapsulation: ViewEncapsulation.None
|
||||||
|
})
|
||||||
|
export class ImageGalleryComponent extends PageComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
@coerceBoolean()
|
||||||
|
pageMode = true;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
mode: 'list' | 'grid' = 'list';
|
||||||
|
|
||||||
|
@ViewChild('searchInput') searchInputField: ElementRef;
|
||||||
|
|
||||||
|
@ViewChild(MatPaginator) paginator: MatPaginator;
|
||||||
|
@ViewChild(MatSort) sort: MatSort;
|
||||||
|
|
||||||
|
@ViewChild(ScrollGridComponent) gridComponent: ScrollGridComponent<ImageResourceInfo, string>;
|
||||||
|
|
||||||
|
defaultPageSize = 10;
|
||||||
|
defaultSortOrder: SortOrder = { property: 'createdTime', direction: Direction.DESC };
|
||||||
|
hidePageSize = false;
|
||||||
|
|
||||||
|
displayedColumns: string[];
|
||||||
|
pageSizeOptions: number[];
|
||||||
|
pageLink: PageLink;
|
||||||
|
|
||||||
|
textSearchMode = false;
|
||||||
|
|
||||||
|
dataSource: ImagesDatasource;
|
||||||
|
|
||||||
|
textSearch = this.fb.control('', {nonNullable: true});
|
||||||
|
|
||||||
|
gridColumns: ScrollGridColumns = {
|
||||||
|
columns: 2,
|
||||||
|
breakpoints: {
|
||||||
|
'screen and (min-width: 2320px)': 10,
|
||||||
|
'screen and (min-width: 2000px)': 8,
|
||||||
|
'gt-lg': 7,
|
||||||
|
'screen and (min-width: 1600px)': 6,
|
||||||
|
'gt-md': 5,
|
||||||
|
'screen and (min-width: 1120px)': 4,
|
||||||
|
'gt-xs': 3
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
gridImagesFetchFunction: GridEntitiesFetchFunction<ImageResourceInfo, string>;
|
||||||
|
gridImagesFilter = '';
|
||||||
|
|
||||||
|
authUser = getCurrentAuthUser(this.store);
|
||||||
|
|
||||||
|
private updateDataSubscription: Subscription;
|
||||||
|
|
||||||
|
private widgetResize$: ResizeObserver;
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
private destroyListMode$: Subject<void>;
|
||||||
|
|
||||||
|
constructor(protected store: Store<AppState>,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private router: Router,
|
||||||
|
private dialog: MatDialog,
|
||||||
|
public translate: TranslateService,
|
||||||
|
private imageService: ImageService,
|
||||||
|
private dialogService: DialogService,
|
||||||
|
private elementRef: ElementRef,
|
||||||
|
private cd: ChangeDetectorRef,
|
||||||
|
private fb: FormBuilder) {
|
||||||
|
super(store);
|
||||||
|
|
||||||
|
this.gridImagesFetchFunction = (pageSize, page, filter) => {
|
||||||
|
const pageLink = new PageLink(pageSize, page, filter, {
|
||||||
|
property: 'createdTime',
|
||||||
|
direction: Direction.DESC
|
||||||
|
});
|
||||||
|
return this.imageService.getImages(pageLink);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.displayedColumns = ['select', 'preview', 'title', 'createdTime', 'resolution', 'size', 'system', 'actions'];
|
||||||
|
let sortOrder: SortOrder = this.defaultSortOrder;
|
||||||
|
this.pageSizeOptions = [this.defaultPageSize, this.defaultPageSize * 2, this.defaultPageSize * 3];
|
||||||
|
const routerQueryParams: PageQueryParam = this.route.snapshot.queryParams;
|
||||||
|
if (this.pageMode) {
|
||||||
|
if (routerQueryParams.hasOwnProperty('direction')
|
||||||
|
|| routerQueryParams.hasOwnProperty('property')) {
|
||||||
|
sortOrder = {
|
||||||
|
property: routerQueryParams?.property || this.defaultSortOrder.property,
|
||||||
|
direction: routerQueryParams?.direction || this.defaultSortOrder.direction
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.pageLink = new PageLink(this.defaultPageSize, 0, null, sortOrder);
|
||||||
|
if (this.pageMode) {
|
||||||
|
if (routerQueryParams.hasOwnProperty('page')) {
|
||||||
|
this.pageLink.page = Number(routerQueryParams.page);
|
||||||
|
}
|
||||||
|
if (routerQueryParams.hasOwnProperty('pageSize')) {
|
||||||
|
this.pageLink.pageSize = Number(routerQueryParams.pageSize);
|
||||||
|
}
|
||||||
|
const textSearchParam = routerQueryParams.textSearch;
|
||||||
|
if (isNotEmptyStr(textSearchParam)) {
|
||||||
|
const decodedTextSearch = decodeURI(textSearchParam);
|
||||||
|
this.textSearchMode = true;
|
||||||
|
this.pageLink.textSearch = decodedTextSearch.trim();
|
||||||
|
this.textSearch.setValue(decodedTextSearch, {emitEvent: false});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.mode === 'list') {
|
||||||
|
this.dataSource = new ImagesDatasource(this.imageService);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
if (this.widgetResize$) {
|
||||||
|
this.widgetResize$.disconnect();
|
||||||
|
}
|
||||||
|
if (this.destroyListMode$) {
|
||||||
|
this.destroyListMode$.next();
|
||||||
|
this.destroyListMode$.complete();
|
||||||
|
}
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterViewInit() {
|
||||||
|
this.textSearch.valueChanges.pipe(
|
||||||
|
debounceTime(150),
|
||||||
|
distinctUntilChanged((prev, current) =>
|
||||||
|
((this.mode === 'list' ? this.pageLink.textSearch : this.gridImagesFilter) ?? '') === current.trim()),
|
||||||
|
takeUntil(this.destroy$)
|
||||||
|
).subscribe(value => {
|
||||||
|
if (this.mode === 'list') {
|
||||||
|
if (this.pageMode) {
|
||||||
|
const queryParams: PageQueryParam = {
|
||||||
|
textSearch: isNotEmptyStr(value) ? encodeURI(value) : null,
|
||||||
|
page: null
|
||||||
|
};
|
||||||
|
this.updatedRouterParamsAndData(queryParams);
|
||||||
|
} else {
|
||||||
|
this.pageLink.textSearch = isNotEmptyStr(value) ? value.trim() : null;
|
||||||
|
this.paginator.pageIndex = 0;
|
||||||
|
this.updateData();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.gridImagesFilter = isNotEmptyStr(value) ? value.trim() : null;
|
||||||
|
this.cd.markForCheck();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.updateMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public setMode(targetMode: 'list' | 'grid') {
|
||||||
|
if (this.mode !== targetMode) {
|
||||||
|
if (this.widgetResize$) {
|
||||||
|
this.widgetResize$.disconnect();
|
||||||
|
this.widgetResize$ = null;
|
||||||
|
}
|
||||||
|
if (this.destroyListMode$) {
|
||||||
|
this.destroyListMode$.next();
|
||||||
|
this.destroyListMode$.complete();
|
||||||
|
this.destroyListMode$ = null;
|
||||||
|
}
|
||||||
|
this.mode = targetMode;
|
||||||
|
if (this.mode === 'list') {
|
||||||
|
this.dataSource = new ImagesDatasource(this.imageService);
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
this.updateMode();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateMode() {
|
||||||
|
if (this.mode === 'list') {
|
||||||
|
this.initListMode();
|
||||||
|
} else {
|
||||||
|
this.initGridMode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private initListMode() {
|
||||||
|
this.destroyListMode$ = new Subject<void>();
|
||||||
|
this.widgetResize$ = new ResizeObserver(() => {
|
||||||
|
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
|
||||||
|
if (showHidePageSize !== this.hidePageSize) {
|
||||||
|
this.hidePageSize = showHidePageSize;
|
||||||
|
this.cd.markForCheck();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.widgetResize$.observe(this.elementRef.nativeElement);
|
||||||
|
if (this.pageMode) {
|
||||||
|
this.route.queryParams.pipe(
|
||||||
|
skip(1),
|
||||||
|
takeUntil(this.destroyListMode$)
|
||||||
|
).subscribe((params: PageQueryParam) => {
|
||||||
|
this.paginator.pageIndex = Number(params.page) || 0;
|
||||||
|
this.paginator.pageSize = Number(params.pageSize) || this.defaultPageSize;
|
||||||
|
this.sort.active = params.property || this.defaultSortOrder.property;
|
||||||
|
this.sort.direction = (params.direction || this.defaultSortOrder.direction).toLowerCase() as SortDirection;
|
||||||
|
const textSearchParam = params.textSearch;
|
||||||
|
if (isNotEmptyStr(textSearchParam)) {
|
||||||
|
const decodedTextSearch = decodeURI(textSearchParam);
|
||||||
|
this.textSearchMode = true;
|
||||||
|
this.pageLink.textSearch = decodedTextSearch.trim();
|
||||||
|
this.textSearch.setValue(decodedTextSearch, {emitEvent: false});
|
||||||
|
} else {
|
||||||
|
this.pageLink.textSearch = null;
|
||||||
|
this.textSearch.reset('', {emitEvent: false});
|
||||||
|
}
|
||||||
|
this.updateData();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.updatePaginationSubscriptions();
|
||||||
|
this.updateData();
|
||||||
|
}
|
||||||
|
|
||||||
|
private initGridMode() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private updatePaginationSubscriptions() {
|
||||||
|
if (this.updateDataSubscription) {
|
||||||
|
this.updateDataSubscription.unsubscribe();
|
||||||
|
this.updateDataSubscription = null;
|
||||||
|
}
|
||||||
|
const sortSubscription$: Observable<object> = this.sort.sortChange.asObservable().pipe(
|
||||||
|
map((data) => {
|
||||||
|
const direction = data.direction.toUpperCase();
|
||||||
|
const queryParams: PageQueryParam = {
|
||||||
|
direction: (this.defaultSortOrder.direction === direction ? null : direction) as Direction,
|
||||||
|
property: this.defaultSortOrder.property === data.active ? null : data.active
|
||||||
|
};
|
||||||
|
queryParams.page = null;
|
||||||
|
this.paginator.pageIndex = 0;
|
||||||
|
return queryParams;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const paginatorSubscription$ = this.paginator.page.asObservable().pipe(
|
||||||
|
map((data) => ({
|
||||||
|
page: data.pageIndex === 0 ? null : data.pageIndex,
|
||||||
|
pageSize: data.pageSize === this.defaultPageSize ? null : data.pageSize
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
this.updateDataSubscription = (merge(sortSubscription$, paginatorSubscription$) as Observable<PageQueryParam>).pipe(
|
||||||
|
takeUntil(this.destroyListMode$)
|
||||||
|
).subscribe(queryParams => this.updatedRouterParamsAndData(queryParams));
|
||||||
|
}
|
||||||
|
|
||||||
|
clearSelection() {
|
||||||
|
this.dataSource.selection.clear();
|
||||||
|
this.cd.detectChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateData() {
|
||||||
|
if (this.mode === 'list') {
|
||||||
|
this.pageLink.page = this.paginator.pageIndex;
|
||||||
|
this.pageLink.pageSize = this.paginator.pageSize;
|
||||||
|
if (this.sort.active) {
|
||||||
|
this.pageLink.sortOrder = {
|
||||||
|
property: this.sort.active,
|
||||||
|
direction: Direction[this.sort.direction.toUpperCase()]
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this.pageLink.sortOrder = null;
|
||||||
|
}
|
||||||
|
this.dataSource.loadEntities(this.pageLink);
|
||||||
|
} else {
|
||||||
|
this.gridComponent.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enterFilterMode() {
|
||||||
|
this.textSearchMode = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.searchInputField.nativeElement.focus();
|
||||||
|
this.searchInputField.nativeElement.setSelectionRange(0, 0);
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
exitFilterMode() {
|
||||||
|
this.textSearchMode = false;
|
||||||
|
this.textSearch.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
trackByEntityId(index: number, entity: BaseData<HasId>) {
|
||||||
|
return entity.id.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
isSystem(image?: ImageResourceInfo): boolean {
|
||||||
|
return image?.tenantId?.id === NULL_UUID;
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteEnabled(image?: ImageResourceInfo): boolean {
|
||||||
|
return this.authUser.authority === Authority.SYS_ADMIN || !this.isSystem(image);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteImage($event: Event, image: ImageResourceInfo) {
|
||||||
|
if ($event) {
|
||||||
|
$event.stopPropagation();
|
||||||
|
}
|
||||||
|
const title = this.translate.instant('image.delete-image-title', {imageTitle: image.title});
|
||||||
|
const content = this.translate.instant('image.delete-image-text');
|
||||||
|
this.dialogService.confirm(title, content,
|
||||||
|
this.translate.instant('action.no'),
|
||||||
|
this.translate.instant('action.yes')).subscribe((result) => {
|
||||||
|
if (result) {
|
||||||
|
this.imageService.deleteImage(imageResourceType(image), image.resourceKey).subscribe(
|
||||||
|
() => {
|
||||||
|
this.updateData();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteImages($event: Event) {
|
||||||
|
if ($event) {
|
||||||
|
$event.stopPropagation();
|
||||||
|
}
|
||||||
|
const selectedImages = this.dataSource.selection.selected;
|
||||||
|
if (selectedImages && selectedImages.length) {
|
||||||
|
const title = this.translate.instant('image.delete-images-title', {count: selectedImages.length});
|
||||||
|
const content = this.translate.instant('image.delete-images-text');
|
||||||
|
this.dialogService.confirm(title, content,
|
||||||
|
this.translate.instant('action.no'),
|
||||||
|
this.translate.instant('action.yes')).subscribe((result) => {
|
||||||
|
if (result) {
|
||||||
|
const tasks = selectedImages.map((image) =>
|
||||||
|
this.imageService.deleteImage(imageResourceType(image), image.resourceKey));
|
||||||
|
forkJoin(tasks).subscribe(
|
||||||
|
() => {
|
||||||
|
this.updateData();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadImage($event, image: ImageResourceInfo) {
|
||||||
|
if ($event) {
|
||||||
|
$event.stopPropagation();
|
||||||
|
}
|
||||||
|
this.imageService.downloadImage(imageResourceType(image), image.resourceKey).subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
exportImage($event, image: ImageResourceInfo) {
|
||||||
|
if ($event) {
|
||||||
|
$event.stopPropagation();
|
||||||
|
}
|
||||||
|
// TODO:
|
||||||
|
}
|
||||||
|
|
||||||
|
importImage(): void {
|
||||||
|
// TODO:
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadImage(): void {
|
||||||
|
this.dialog.open<UploadImageDialogComponent, any,
|
||||||
|
boolean>(UploadImageDialogComponent, {
|
||||||
|
disableClose: true,
|
||||||
|
panelClass: ['tb-dialog', 'tb-fullscreen-dialog']
|
||||||
|
}).afterClosed().subscribe((result) => {
|
||||||
|
if (result) {
|
||||||
|
this.updateData();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
editImage($event, image: ImageResourceInfo) {
|
||||||
|
if ($event) {
|
||||||
|
$event.stopPropagation();
|
||||||
|
}
|
||||||
|
// TODO:
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updatedRouterParamsAndData(queryParams: object, queryParamsHandling: QueryParamsHandling = 'merge') {
|
||||||
|
if (this.pageMode) {
|
||||||
|
this.router.navigate([], {
|
||||||
|
relativeTo: this.route,
|
||||||
|
queryParams,
|
||||||
|
queryParamsHandling
|
||||||
|
});
|
||||||
|
if (queryParamsHandling === '' && isEqual(this.route.snapshot.queryParams, queryParams)) {
|
||||||
|
this.updateData();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.updateData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class ImagesDatasource implements DataSource<ImageResourceInfo> {
|
||||||
|
private entitiesSubject = new BehaviorSubject<ImageResourceInfo[]>([]);
|
||||||
|
private pageDataSubject = new BehaviorSubject<PageData<ImageResourceInfo>>(emptyPageData<ImageResourceInfo>());
|
||||||
|
|
||||||
|
public pageData$ = this.pageDataSubject.asObservable();
|
||||||
|
|
||||||
|
public selection = new SelectionModel<ImageResourceInfo>(true, []);
|
||||||
|
|
||||||
|
public dataLoading = true;
|
||||||
|
|
||||||
|
constructor(private imageService: ImageService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(collectionViewer: CollectionViewer):
|
||||||
|
Observable<ImageResourceInfo[] | ReadonlyArray<ImageResourceInfo>> {
|
||||||
|
return this.entitiesSubject.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect(collectionViewer: CollectionViewer): void {
|
||||||
|
this.entitiesSubject.complete();
|
||||||
|
this.pageDataSubject.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
const pageData = emptyPageData<ImageResourceInfo>();
|
||||||
|
this.entitiesSubject.next(pageData.data);
|
||||||
|
this.pageDataSubject.next(pageData);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadEntities(pageLink: PageLink): Observable<PageData<ImageResourceInfo>> {
|
||||||
|
this.dataLoading = true;
|
||||||
|
const result = new ReplaySubject<PageData<ImageResourceInfo>>();
|
||||||
|
this.fetchEntities(pageLink).pipe(
|
||||||
|
tap(() => {
|
||||||
|
this.selection.clear();
|
||||||
|
}),
|
||||||
|
catchError(() => of(emptyPageData<ImageResourceInfo>())),
|
||||||
|
).subscribe(
|
||||||
|
(pageData) => {
|
||||||
|
this.entitiesSubject.next(pageData.data);
|
||||||
|
this.pageDataSubject.next(pageData);
|
||||||
|
result.next(pageData);
|
||||||
|
this.dataLoading = false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchEntities(pageLink: PageLink): Observable<PageData<ImageResourceInfo>> {
|
||||||
|
return this.imageService.getImages(pageLink);
|
||||||
|
}
|
||||||
|
|
||||||
|
isAllSelected(): Observable<boolean> {
|
||||||
|
const numSelected = this.selection.selected.length;
|
||||||
|
return this.entitiesSubject.pipe(
|
||||||
|
map((entities) => numSelected === entities.length)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
isEmpty(): Observable<boolean> {
|
||||||
|
return this.entitiesSubject.pipe(
|
||||||
|
map((entities) => !entities.length)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
total(): Observable<number> {
|
||||||
|
return this.pageDataSubject.pipe(
|
||||||
|
map((pageData) => pageData.totalElements)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
masterToggle() {
|
||||||
|
const entities = this.entitiesSubject.getValue();
|
||||||
|
const numSelected = this.selection.selected.length;
|
||||||
|
if (numSelected === entities.length) {
|
||||||
|
this.selection.clear();
|
||||||
|
} else {
|
||||||
|
entities.forEach(row => {
|
||||||
|
this.selection.select(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
<!--
|
||||||
|
|
||||||
|
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]="uploadImageFormGroup" (ngSubmit)="upload()" style="width: 560px;">
|
||||||
|
<mat-toolbar color="primary">
|
||||||
|
<h2>{{ 'image.upload-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">
|
||||||
|
<tb-image-input resultAsFile
|
||||||
|
label="{{'image.image-preview' | translate}}"
|
||||||
|
formControlName="file"
|
||||||
|
(fileNameChanged)="imageFileNameChanged($event)">
|
||||||
|
</tb-image-input>
|
||||||
|
<mat-form-field *ngIf="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')">
|
||||||
|
{{ 'image.name-required' | translate }}
|
||||||
|
</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
<div mat-dialog-actions fxLayout="row" fxLayoutAlign="end center">
|
||||||
|
<button mat-button color="primary"
|
||||||
|
type="button"
|
||||||
|
[disabled]="(isLoading$ | async)"
|
||||||
|
(click)="cancel()" cdkFocusInitial>
|
||||||
|
{{ 'action.cancel' | translate }}
|
||||||
|
</button>
|
||||||
|
<button mat-raised-button color="primary"
|
||||||
|
type="submit"
|
||||||
|
[disabled]="(isLoading$ | async) || uploadImageFormGroup.invalid
|
||||||
|
|| !uploadImageFormGroup.dirty">
|
||||||
|
{{ 'action.upload' | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
@ -0,0 +1,90 @@
|
|||||||
|
///
|
||||||
|
/// 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 { Component, OnInit, SkipSelf } from '@angular/core';
|
||||||
|
import { ErrorStateMatcher } from '@angular/material/core';
|
||||||
|
import { 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';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'tb-upload-image-dialog',
|
||||||
|
templateUrl: './upload-image-dialog.component.html',
|
||||||
|
providers: [{provide: ErrorStateMatcher, useExisting: UploadImageDialogComponent}],
|
||||||
|
styleUrls: []
|
||||||
|
})
|
||||||
|
export class UploadImageDialogComponent extends
|
||||||
|
DialogComponent<UploadImageDialogComponent, boolean> implements OnInit, ErrorStateMatcher {
|
||||||
|
|
||||||
|
uploadImageFormGroup: UntypedFormGroup;
|
||||||
|
|
||||||
|
submitted = false;
|
||||||
|
|
||||||
|
constructor(protected store: Store<AppState>,
|
||||||
|
protected router: Router,
|
||||||
|
private imageService: ImageService,
|
||||||
|
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
|
||||||
|
public dialogRef: MatDialogRef<UploadImageDialogComponent, boolean>,
|
||||||
|
public fb: UntypedFormBuilder) {
|
||||||
|
super(store, router, dialogRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.uploadImageFormGroup = this.fb.group({
|
||||||
|
file: [null, [Validators.required]],
|
||||||
|
title: [null, [Validators.required]]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
imageFileNameChanged(fileName: string) {
|
||||||
|
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);
|
||||||
|
const customErrorState = !!(control && control.invalid && this.submitted);
|
||||||
|
return originalErrorState || customErrorState;
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel(): void {
|
||||||
|
this.dialogRef.close(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -90,6 +90,10 @@ export class ScrollGridDatasource<T, F> extends DataSource<(T | GridCellType)[]>
|
|||||||
|
|
||||||
public updateFilter(filter: F) {
|
public updateFilter(filter: F) {
|
||||||
this.filter = filter;
|
this.filter = filter;
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
public update() {
|
||||||
if (this.active) {
|
if (this.active) {
|
||||||
const prevLength = this._rows.length;
|
const prevLength = this._rows.length;
|
||||||
this._reset();
|
this._reset();
|
||||||
|
|||||||
@ -39,6 +39,7 @@ import { TwoFactorAuthSettingsComponent } from '@home/pages/admin/two-factor-aut
|
|||||||
import { widgetsLibraryRoutes } from '@home/pages/widget/widget-library-routing.module';
|
import { widgetsLibraryRoutes } from '@home/pages/widget/widget-library-routing.module';
|
||||||
import { RouterTabsComponent } from '@home/components/router-tabs.component';
|
import { RouterTabsComponent } from '@home/components/router-tabs.component';
|
||||||
import { auditLogsRoutes } from '@home/pages/audit-log/audit-log-routing.module';
|
import { auditLogsRoutes } from '@home/pages/audit-log/audit-log-routing.module';
|
||||||
|
import { ImageGalleryComponent } from '@home/components/image/image-gallery.component';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OAuth2LoginProcessingUrlResolver implements Resolve<string> {
|
export class OAuth2LoginProcessingUrlResolver implements Resolve<string> {
|
||||||
@ -71,6 +72,25 @@ const routes: Routes = [
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
...widgetsLibraryRoutes,
|
...widgetsLibraryRoutes,
|
||||||
|
{
|
||||||
|
path: 'images',
|
||||||
|
data: {
|
||||||
|
breadcrumb: {
|
||||||
|
label: 'image.gallery',
|
||||||
|
icon: 'filter'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: ImageGalleryComponent,
|
||||||
|
data: {
|
||||||
|
auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN],
|
||||||
|
title: 'image.gallery',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'resources-library',
|
path: 'resources-library',
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@ -14,7 +14,16 @@
|
|||||||
/// limitations under the License.
|
/// limitations under the License.
|
||||||
///
|
///
|
||||||
|
|
||||||
import { AfterViewInit, ChangeDetectorRef, Component, forwardRef, Input, OnDestroy, ViewChild } from '@angular/core';
|
import {
|
||||||
|
AfterViewInit,
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Component, EventEmitter,
|
||||||
|
forwardRef,
|
||||||
|
Input,
|
||||||
|
OnDestroy,
|
||||||
|
Output,
|
||||||
|
ViewChild
|
||||||
|
} from '@angular/core';
|
||||||
import { PageComponent } from '@shared/components/page.component';
|
import { PageComponent } from '@shared/components/page.component';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { AppState } from '@core/core.state';
|
import { AppState } from '@core/core.state';
|
||||||
@ -27,6 +36,7 @@ import { UtilsService } from '@core/services/utils.service';
|
|||||||
import { DialogService } from '@core/services/dialog.service';
|
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';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'tb-image-input',
|
selector: 'tb-image-input',
|
||||||
@ -74,7 +84,16 @@ export class ImageInputComponent extends PageComponent implements AfterViewInit,
|
|||||||
@Input()
|
@Input()
|
||||||
inputId = this.utils.guid();
|
inputId = this.utils.guid();
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
@coerceBoolean()
|
||||||
|
resultAsFile = false;
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
fileNameChanged = new EventEmitter<string>();
|
||||||
|
|
||||||
imageUrl: string;
|
imageUrl: string;
|
||||||
|
file: File;
|
||||||
|
fileName: string;
|
||||||
safeImageUrl: SafeUrl;
|
safeImageUrl: SafeUrl;
|
||||||
|
|
||||||
@ViewChild('flow', {static: true})
|
@ViewChild('flow', {static: true})
|
||||||
@ -97,7 +116,9 @@ export class ImageInputComponent extends PageComponent implements AfterViewInit,
|
|||||||
ngAfterViewInit() {
|
ngAfterViewInit() {
|
||||||
this.autoUploadSubscription = this.flow.events$.subscribe(event => {
|
this.autoUploadSubscription = this.flow.events$.subscribe(event => {
|
||||||
if (event.type === 'fileAdded') {
|
if (event.type === 'fileAdded') {
|
||||||
const file = (event.event[0] as flowjs.FlowFile).file;
|
const flowFile = event.event[0] as flowjs.FlowFile;
|
||||||
|
const file = flowFile.file;
|
||||||
|
const fileName = flowFile.name;
|
||||||
if (this.maxSizeByte && this.maxSizeByte < file.size) {
|
if (this.maxSizeByte && this.maxSizeByte < file.size) {
|
||||||
this.dialog.alert(
|
this.dialog.alert(
|
||||||
this.translate.instant('dashboard.cannot-upload-file'),
|
this.translate.instant('dashboard.cannot-upload-file'),
|
||||||
@ -108,10 +129,12 @@ export class ImageInputComponent extends PageComponent implements AfterViewInit,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (loadEvent) => {
|
reader.onload = (_loadEvent) => {
|
||||||
if (typeof reader.result === 'string' && reader.result.startsWith('data:image/')) {
|
if (typeof reader.result === 'string' && reader.result.startsWith('data:image/')) {
|
||||||
this.imageUrl = reader.result;
|
this.imageUrl = reader.result;
|
||||||
this.safeImageUrl = this.sanitizer.bypassSecurityTrustUrl(this.imageUrl);
|
this.safeImageUrl = this.sanitizer.bypassSecurityTrustUrl(this.imageUrl);
|
||||||
|
this.file = file;
|
||||||
|
this.fileName = fileName;
|
||||||
this.updateModel();
|
this.updateModel();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -146,12 +169,19 @@ export class ImageInputComponent extends PageComponent implements AfterViewInit,
|
|||||||
|
|
||||||
private updateModel() {
|
private updateModel() {
|
||||||
this.cd.markForCheck();
|
this.cd.markForCheck();
|
||||||
this.propagateChange(this.imageUrl);
|
if (this.resultAsFile) {
|
||||||
|
this.propagateChange(this.file);
|
||||||
|
} else {
|
||||||
|
this.propagateChange(this.imageUrl);
|
||||||
|
}
|
||||||
|
this.fileNameChanged.emit(this.fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
clearImage() {
|
clearImage() {
|
||||||
this.imageUrl = null;
|
this.imageUrl = null;
|
||||||
this.safeImageUrl = null;
|
this.safeImageUrl = null;
|
||||||
|
this.file = null;
|
||||||
|
this.fileName = null;
|
||||||
this.updateModel();
|
this.updateModel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -77,7 +77,8 @@
|
|||||||
"show-more": "Show more",
|
"show-more": "Show more",
|
||||||
"dont-show-again": "Do not show again",
|
"dont-show-again": "Do not show again",
|
||||||
"see-documentation": "See documentation",
|
"see-documentation": "See documentation",
|
||||||
"clear": "Clear"
|
"clear": "Clear",
|
||||||
|
"upload": "Upload"
|
||||||
},
|
},
|
||||||
"aggregation": {
|
"aggregation": {
|
||||||
"aggregation": "Aggregation",
|
"aggregation": "Aggregation",
|
||||||
@ -2942,6 +2943,31 @@
|
|||||||
"browse-file": "Browse file",
|
"browse-file": "Browse file",
|
||||||
"browse-files": "Browse files"
|
"browse-files": "Browse files"
|
||||||
},
|
},
|
||||||
|
"image": {
|
||||||
|
"gallery": "Image gallery",
|
||||||
|
"search": "Search image",
|
||||||
|
"selected-images": "{ count, plural, =1 {1 image} other {# images} } selected",
|
||||||
|
"created-time": "Created time",
|
||||||
|
"name": "Name",
|
||||||
|
"name-required": "Name is required.",
|
||||||
|
"resolution": "Resolution",
|
||||||
|
"size": "Size",
|
||||||
|
"system": "System",
|
||||||
|
"download-image": "Download image",
|
||||||
|
"export-image": "Export image",
|
||||||
|
"import-image": "Import image",
|
||||||
|
"upload-image": "Upload image",
|
||||||
|
"edit-image": "Edit image",
|
||||||
|
"no-images": "No images found",
|
||||||
|
"delete-image": "Delete image",
|
||||||
|
"delete-image-title": "Are you sure you want to delete image '{{imageTitle}}'?",
|
||||||
|
"delete-image-text": "Be careful, after the confirmation image will become unrecoverable.",
|
||||||
|
"delete-images-title": "Are you sure you want to delete { count, plural, =1 {1 image} other {# images} }?",
|
||||||
|
"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-input": {
|
"image-input": {
|
||||||
"drop-images-or": "Drag and drop an images or",
|
"drop-images-or": "Drag and drop an images or",
|
||||||
"drag-and-drop": "Drag & Drop",
|
"drag-and-drop": "Drag & Drop",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user