html模块 deleteImage点击事件
<div class="tb-images tb-absolute-fill" [ngClass]="{'tb-dialog-mode': dialogMode, 'mat-padding': !dialogMode}">
<div fxFlex fxLayout="column" class="tb-images-content" [ngClass]="{'tb-outlined-border': pageMode}">
<mat-toolbar class="mat-mdc-table-toolbar"
fxLayout.lt-lg="column" fxLayoutAlign.lt-lg="start stretch"
[fxShow]="!textSearchMode && (mode === 'grid' || dataSource?.selection.isEmpty())" [ngClass.lt-lg]="{'multi-row': !isSysAdmin}">
<div fxFlex fxLayout="row" fxLayoutAlign="start center">
<div class="mat-toolbar-tools">
<span fxHide fxShow.gt-sm 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>
<mat-slide-toggle *ngIf="!isSysAdmin"
fxHide fxShow.gt-md
[ngModel]="includeSystemImages"
(ngModelChange)="includeSystemImagesChanged($event)">{{ 'image.include-system-images' | translate }}李发林</mat-slide-toggle>
</div>
<section fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap.gt-md="16px">
<span fxFlex></span>
<section fxLayout="row" fxLayoutAlign="start center">
<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">
<tb-icon>mdi:file-import</tb-icon>
</button>
<button fxHide fxShow.lt-lg
[disabled]="isLoading$ | async"
mat-icon-button
color="primary"
(click)="uploadImage()"
matTooltip="{{'image.upload-image' | translate }}"
matTooltipPosition="above">
<mat-icon>add</mat-icon>
</button>
</section>
<button fxHide fxShow.gt-md
[disabled]="isLoading$ | async"
mat-flat-button
color="primary"
(click)="uploadImage()">
{{ 'image.upload-image' | translate }}
</button>
</section>
</div>
<mat-slide-toggle *ngIf="!isSysAdmin" fxHide fxShow.lt-lg
[ngModel]="includeSystemImages"
(ngModelChange)="includeSystemImagesChanged($event)">{{ 'image.include-system-images' | translate }}</mat-slide-toggle>
</mat-toolbar>
<mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="textSearchMode && (mode === 'grid' || 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]="mode === 'list' && !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]="trackByEntity"
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()"
[fxShow]="deleteEnabled(image)"
(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">
<div class="tb-image-preview-cell">
<img class="tb-image-preview" [src]="image.link | image: {preview: true} | async" alt="{{ image.title }}">
</div>
</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: 160px; min-width: 160px;"> {{ '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: 80px; min-width: 80px;"> {{ '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: 80px; min-width: 80px;"> {{ '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 [ngStyle.gt-md]="{ minWidth: '240px', maxWidth: '240px', width: '240px' }">
</mat-header-cell>
<mat-cell *matCellDef="let image" [ngStyle.gt-md]="{ minWidth: '240px', maxWidth: '240px', width: '240px' }">
<div fxHide fxShow.gt-md 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.embed-image' | translate }}"
matTooltipPosition="above"
(click)="embedImage($event, image)">
<mat-icon>code</mat-icon>
</button>
<button mat-icon-button [disabled]="isLoading$ | async"
matTooltip="{{ (readonly(image) ? 'image.image-details' : '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>
<div fxHide fxShow.lt-lg>
<button mat-icon-button
(click)="$event.stopPropagation()"
[matMenuTriggerFor]="cellActionsMenu">
<mat-icon class="material-icons">more_vert</mat-icon>
</button>
<mat-menu #cellActionsMenu="matMenu" xPosition="before">
<button mat-menu-item
[disabled]="isLoading$ | async"
(click)="downloadImage($event, image)">
<mat-icon>file_download</mat-icon>
<span translate>image.download-image</span>
</button>
<button mat-menu-item
[disabled]="isLoading$ | async"
(click)="exportImage($event, image)">
<tb-icon matMenuItemIcon>mdi:file-export</tb-icon>
<span translate>image.export-image</span>
</button>
<button mat-menu-item
[disabled]="isLoading$ | async"
(click)="embedImage($event, image)">
<mat-icon>code</mat-icon>
<span>{{ 'image.embed-image' | translate }}</span>
</button>
<button mat-menu-item
[disabled]="isLoading$ | async"
(click)="editImage($event, image)">
<mat-icon>edit</mat-icon>
<span>{{ (readonly(image) ? 'image.image-details' : 'image.edit-image') | translate }}</span>
</button>
<button mat-menu-item
[disabled]="isLoading$ | async"
[fxShow]="deleteEnabled(image)"
(click)="deleteImage($event, image)">
<mat-icon>delete</mat-icon>
<span translate>image.delete-image</span>
</button>
</mat-menu>
</div>
</mat-cell>
</ng-container>
<ng-container matColumnDef="imageSelect" stickyEnd>
<mat-header-cell *matHeaderCellDef></mat-header-cell>
<mat-cell *matCellDef="let image">
<button mat-stroked-button
color="primary"
(click)="selectImage($event, image)">{{ 'action.select' | translate }}</button>
</mat-cell>
</ng-container>
<mat-header-row [ngClass]="{'mat-row-select': true}" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
<mat-row [fxShow]="!dataSource.dataLoading"
[ngClass]="{'mat-row-select': true,
'mat-selected': dataSource.selection.isSelected(image)}"
*matRowDef="let image; columns: displayedColumns;" (click)="rowClick($event, 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 *ngIf="mode === 'grid'" fxFlex [ngClass]="{'mat-padding': !dialogMode}" fxLayout="column">
<tb-scroll-grid fxFlex
[columns]="gridColumns"
[itemSize]="gridImagesItemSizeStrategy"
[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>
{{ 'common.loading' | translate }}
</span>
</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" let-itemIndex="itemIndex">
<div class="tb-image-card tb-primary-fill">
<div class="tb-image-card-overlay">
<div *ngIf="!selectionMode" class="tb-image-card-overlay-buttons">
<button mat-icon-button [disabled]="isLoading$ | async"
matTooltip="{{ 'image.download-image' | translate }}"
matTooltipPosition="above"
(click)="downloadImage($event, item)">
<mat-icon>file_download</mat-icon>
</button>
<button mat-icon-button [disabled]="isLoading$ | async"
matTooltip="{{ 'image.export-image' | translate }}"
matTooltipPosition="above"
(click)="exportImage($event, item)">
<tb-icon>mdi:file-export</tb-icon>
</button>
<button mat-icon-button [disabled]="isLoading$ | async"
matTooltip="{{ 'image.embed-image' | translate }}"
matTooltipPosition="above"
(click)="embedImage($event, item, itemIndex)">
<mat-icon>code</mat-icon>
</button>
<button *ngIf="deleteEnabled(item)"
mat-icon-button [disabled]="(isLoading$ | async)"
matTooltip="{{ 'image.delete-image' | translate }}"
matTooltipPosition="above"
(click)="deleteImage($event, item, itemIndex)">
<mat-icon>delete</mat-icon>
</button>
</div>
</div>
<div class="tb-image-preview-container">
<div class="tb-image-preview-overlay" (click)="selectionMode && selectImage($event, item)">
<button
*ngIf="!selectionMode"
mat-flat-button
color="primary"
(click)="editImage($event, item, itemIndex)">
{{ (readonly(item) ? 'image.image-details' : 'image.edit-image') | translate }}
</button>
<button
*ngIf="selectionMode"
mat-flat-button
color="primary"
(click)="selectImage($event, item)">{{ 'action.select' | translate }}
</button>
</div>
<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 class="tb-image-card tb-primary-fill loading-cell">
<div class="tb-image-preview-container">
<div class="tb-image-preview-spacer"></div>
<div class="tb-image-preview"></div>
</div>
<div class="tb-image-details"></div>
</div>
</ng-template>
找到deleteImage方法 调用imageService服务 image.service.ts文件下的deleteImage方法
///
/// Copyright © 2016-2024 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 {
ImageResourceInfo,
ImageResourceInfoWithReferences,
imageResourceType,
toImageDeleteResult
} from '@shared/models/resource.models';
import { forkJoin, merge, Observable, of, Subject, Subscription } from 'rxjs';
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 } from 'rxjs/operators';
import {
AfterViewInit,
ChangeDetectorRef,
Component,
ElementRef, EventEmitter, HostBinding,
Input,
OnDestroy,
OnInit, Output,
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 { 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, parseHttpErrorMessage } 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 '@shared/components/grid/scroll-grid-datasource';
import { ItemSizeStrategy, ScrollGridComponent } from '@shared/components/grid/scroll-grid.component';
import { MatDialog } from '@angular/material/dialog';
import {
UploadImageDialogComponent,
UploadImageDialogData
} from '@shared/components/image/upload-image-dialog.component';
import { ImageDialogComponent, ImageDialogData } from '@shared/components/image/image-dialog.component';
import { ImportExportService } from '@shared/import-export/import-export.service';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import {
ImagesInUseDialogComponent,
ImagesInUseDialogData
} from '@shared/components/image/images-in-use-dialog.component';
import { ImagesDatasource } from '@shared/components/image/images-datasource';
import { EmbedImageDialogComponent, EmbedImageDialogData } from '@shared/components/image/embed-image-dialog.component';
interface GridImagesFilter {
search: string;
includeSystemImages: boolean;
}
const pageGridColumns: 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
}
};
const dialogGridColumns: ScrollGridColumns = {
columns: 2,
breakpoints: {
'gt-md': 4,
'gt-xs': 3
}
};
@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 {
@HostBinding('style.display')
private display = 'block';
@HostBinding('style.width')
private width = '100%';
@HostBinding('style.height')
private height = '100%';
@Input()
@coerceBoolean()
pageMode = true;
@Input()
@coerceBoolean()
dialogMode = false;
@Input()
mode: 'list' | 'grid' = 'list';
@Input()
@coerceBoolean()
selectionMode = false;
@Output()
imageSelected = new EventEmitter<ImageResourceInfo>();
@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});
includeSystemImages = false;
gridColumns: ScrollGridColumns;
gridImagesFetchFunction: GridEntitiesFetchFunction<ImageResourceInfo, GridImagesFilter>;
gridImagesFilter: GridImagesFilter = {
search: '',
includeSystemImages: false
};
gridImagesItemSizeStrategy: ItemSizeStrategy = {
defaultItemSize: 200,
itemSizeFunction: itemWidth => itemWidth + 72
};
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 importExportService: ImportExportService,
private elementRef: ElementRef,
private cd: ChangeDetectorRef,
private fb: FormBuilder) {
super(store);
this.gridImagesFetchFunction = (pageSize, page, filter) => {
const pageLink = new PageLink(pageSize, page, filter.search, {
property: 'createdTime',
direction: Direction.DESC
});
return this.imageService.getImages(pageLink, filter.includeSystemImages);
};
}
ngOnInit(): void {
this.gridColumns = this.dialogMode ? dialogGridColumns : pageGridColumns;
this.displayedColumns = this.computeDisplayedColumns();
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, null,
entity => this.deleteEnabled(entity));
}
}
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.search) ?? '') === 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 = {
search: isNotEmptyStr(value) ? value.trim() : null,
includeSystemImages: this.includeSystemImages
};
this.cd.markForCheck();
}
});
this.updateMode();
}
public includeSystemImagesChanged(value: boolean) {
this.includeSystemImages = value;
this.displayedColumns = this.computeDisplayedColumns();
this.gridImagesFilter = {
search: this.gridImagesFilter.search,
includeSystemImages: this.includeSystemImages
};
if (this.mode === 'list') {
this.paginator.pageIndex = 0;
this.updateData();
} else {
this.cd.markForCheck();
}
}
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, null,
entity => this.deleteEnabled(entity));
}
setTimeout(() => {
this.updateMode();
});
}
}
public get isSysAdmin(): boolean {
return this.authUser.authority === Authority.SYS_ADMIN;
}
private computeDisplayedColumns(): string[] {
let columns: string[];
if (this.selectionMode) {
columns = ['preview', 'title'];
if (!this.isSysAdmin && this.includeSystemImages) {
columns.push('system');
}
columns.push('imageSelect');
} else {
columns = ['select', 'preview', 'title', 'createdTime', 'resolution', 'size'];
if (!this.isSysAdmin && this.includeSystemImages) {
columns.push('system');
}
columns.push('actions');
}
return columns;
}
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;
}
console.log('点击删除按钮,李发林,获取新的数据',this.pageLink)
this.dataSource.loadEntities(this.pageLink, this.includeSystemImages);
} else {
this.gridComponent.update();
}
}
private imageUpdated(image: ImageResourceInfo, index = -1) {
if (this.mode === 'list') {
this.updateData();
} else {
this.gridComponent.updateItem(index, image);
}
}
private imageDeleted(index = -1) {
console.log('点击删除按钮,李发林,删除成功,刷新数据,判断mode',this.mode)
if (this.mode === 'list') {
this.updateData();
} else {
this.gridComponent.deleteItem(index);
}
}
enterFilterMode() {
this.textSearchMode = true;
setTimeout(() => {
this.searchInputField.nativeElement.focus();
this.searchInputField.nativeElement.setSelectionRange(0, 0);
}, 10);
}
exitFilterMode() {
this.textSearchMode = false;
this.textSearch.reset();
}
trackByEntity(index: number, entity: BaseData<HasId>) {
return entity;
}
isSystem(image?: ImageResourceInfo): boolean {
return !this.isSysAdmin && image?.tenantId?.id === NULL_UUID;
}
readonly(image?: ImageResourceInfo): boolean {
return this.authUser.authority !== Authority.SYS_ADMIN && this.isSystem(image);
}
deleteEnabled(image?: ImageResourceInfo): boolean {
return this.authUser.authority === Authority.SYS_ADMIN || !this.isSystem(image);
}
deleteImage($event: Event, image: ImageResourceInfo, itemIndex = -1) {
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) {
console.log('点击删除按钮,李发林')
this.imageService.deleteImage(imageResourceType(image), image.resourceKey, false, {ignoreErrors: true}).pipe(
map(() => toImageDeleteResult(image)),
catchError((err) => of(toImageDeleteResult(image, err)))
).subscribe(
(deleteResult) => {
console.log('点击删除按钮,李发林,有返回值',deleteResult)
if (deleteResult.success) {
this.imageDeleted(itemIndex);
} else if (deleteResult.imageIsReferencedError) {
this.dialog.open<ImagesInUseDialogComponent, ImagesInUseDialogData,
ImageResourceInfo[]>(ImagesInUseDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
multiple: false,
images: [{...image, ...{references: deleteResult.references}}]
}
}).afterClosed().subscribe((images) => {
if (images) {
this.imageService.deleteImage(imageResourceType(image), image.resourceKey, true).subscribe(
() => {
this.imageDeleted(itemIndex);
}
);
}
});
} else {
const errorMessageWithTimeout = parseHttpErrorMessage(deleteResult.error, this.translate);
setTimeout(() => {
this.store.dispatch(new ActionNotificationShow({message: errorMessageWithTimeout.message, type: 'error'}));
}, errorMessageWithTimeout.timeout);
}
});
}
});
}
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, false, {ignoreErrors: true}).pipe(
map(() => toImageDeleteResult(image)),
catchError((err) => of(toImageDeleteResult(image, err)))
)
);
forkJoin(tasks).subscribe(
(deleteResults) => {
const anySuccess = deleteResults.some(res => res.success);
const referenceErrors = deleteResults.filter(res => res.imageIsReferencedError);
const otherError = deleteResults.find(res => !res.success);
if (anySuccess) {
this.updateData();
}
if (referenceErrors?.length) {
const imagesWithReferences: ImageResourceInfoWithReferences[] =
referenceErrors.map(ref => ({...ref.image, ...{references: ref.references}}));
this.dialog.open<ImagesInUseDialogComponent, ImagesInUseDialogData,
ImageResourceInfo[]>(ImagesInUseDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
multiple: true,
images: imagesWithReferences
}
}).afterClosed().subscribe((forceDeleteImages) => {
if (forceDeleteImages && forceDeleteImages.length) {
const forceDeleteTasks = forceDeleteImages.map((image) =>
this.imageService.deleteImage(imageResourceType(image), image.resourceKey, true)
);
forkJoin(forceDeleteTasks).subscribe(
() => {
this.updateData();
}
);
}
});
} else if (otherError) {
const errorMessageWithTimeout = parseHttpErrorMessage(otherError.error, this.translate);
setTimeout(() => {
this.store.dispatch(new ActionNotificationShow({message: errorMessageWithTimeout.message, type: 'error'}));
}, errorMessageWithTimeout.timeout);
}
}
);
}
});
}
}
downloadImage($event, image: ImageResourceInfo) {
if ($event) {
$event.stopPropagation();
}
this.imageService.downloadImage(imageResourceType(image), image.resourceKey).subscribe();
}
exportImage($event, image: ImageResourceInfo) {
if ($event) {
$event.stopPropagation();
}
this.importExportService.exportImage(imageResourceType(image), image.resourceKey);
}
importImage(): void {
this.importExportService.importImage().subscribe((image) => {
if (image) {
if (this.selectionMode) {
this.imageSelected.next(image);
} else {
this.updateData();
}
}
});
}
selectImage($event, image: ImageResourceInfo) {
if ($event) {
$event.stopPropagation();
}
this.imageSelected.next(image);
}
rowClick($event, image: ImageResourceInfo) {
if (this.selectionMode) {
this.selectImage($event, image);
} else {
if (this.deleteEnabled(image)) {
this.dataSource.selection.toggle(image);
}
}
}
uploadImage(): void {
this.dialog.open<UploadImageDialogComponent, UploadImageDialogData,
ImageResourceInfo>(UploadImageDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {}
}).afterClosed().subscribe((result) => {
if (result) {
if (this.selectionMode) {
this.imageSelected.next(result);
} else {
this.updateData();
}
}
});
}
editImage($event: Event, image: ImageResourceInfo, itemIndex = -1) {
if ($event) {
$event.stopPropagation();
}
this.dialog.open<ImageDialogComponent, ImageDialogData,
ImageResourceInfo>(ImageDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
image,
readonly: this.readonly(image)
}
}).afterClosed().subscribe((result) => {
if (result) {
this.imageUpdated(result, itemIndex);
}
});
}
embedImage($event: Event, image: ImageResourceInfo, itemIndex = -1) {
if ($event) {
$event.stopPropagation();
}
this.dialog.open<EmbedImageDialogComponent, EmbedImageDialogData,
ImageResourceInfo>(EmbedImageDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
image,
readonly: this.readonly(image)
}
}).afterClosed().subscribe((result) => {
if (result) {
this.imageUpdated(result, itemIndex);
}
});
}
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();
}
}
}
///
/// Copyright © 2016-2024 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 { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { PageLink } from '@shared/models/page/page-link';
import { defaultHttpOptionsFromConfig, defaultHttpUploadOptions, RequestConfig } from '@core/http/http-utils';
import { Observable, of, ReplaySubject } from 'rxjs';
import { PageData } from '@shared/models/page/page-data';
import {
NO_IMAGE_DATA_URI,
ImageResourceInfo,
imageResourceType,
ImageResourceType,
IMAGES_URL_PREFIX, isImageResourceUrl, ImageExportData, removeTbImagePrefix
} from '@shared/models/resource.models';
import { catchError, map, switchMap } from 'rxjs/operators';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { blobToBase64 } from '@core/utils';
import { ResourcesService } from '@core/services/resources.service';
@Injectable({
providedIn: 'root'
})
export class ImageService {
private imagesLoading: { [url: string]: ReplaySubject<Blob> } = {};
constructor(
private http: HttpClient,
private sanitizer: DomSanitizer,
private resourcesService: ResourcesService
) {
}
public uploadImage(file: File, title: string, config?: RequestConfig): Observable<ImageResourceInfo> {
if (!config) {
config = {};
}
const formData = new FormData();
formData.append('file', file);
formData.append('title', title);
return this.http.post<ImageResourceInfo>('/api/image', formData,
defaultHttpUploadOptions(config.ignoreLoading, config.ignoreErrors, config.resendRequest));
}
public updateImage(type: ImageResourceType, key: string, file: File, config?: RequestConfig): Observable<ImageResourceInfo> {
if (!config) {
config = {};
}
const formData = new FormData();
formData.append('file', file);
return this.http.put<ImageResourceInfo>(`${IMAGES_URL_PREFIX}/${type}/${encodeURIComponent(key)}`, formData,
defaultHttpUploadOptions(config.ignoreLoading, config.ignoreErrors, config.resendRequest));
}
public updateImageInfo(imageInfo: ImageResourceInfo, config?: RequestConfig): Observable<ImageResourceInfo> {
const type = imageResourceType(imageInfo);
const key = encodeURIComponent(imageInfo.resourceKey);
return this.http.put<ImageResourceInfo>(`${IMAGES_URL_PREFIX}/${type}/${key}/info`,
imageInfo, defaultHttpOptionsFromConfig(config));
}
public updateImagePublicStatus(imageInfo: ImageResourceInfo, isPublic: boolean, config?: RequestConfig): Observable<ImageResourceInfo> {
const type = imageResourceType(imageInfo);
const key = encodeURIComponent(imageInfo.resourceKey);
return this.http.put<ImageResourceInfo>(`${IMAGES_URL_PREFIX}/${type}/${key}/public/${isPublic}`,
imageInfo, defaultHttpOptionsFromConfig(config));
}
public getImages(pageLink: PageLink, includeSystemImages = false, config?: RequestConfig): Observable<PageData<ImageResourceInfo>> {
return this.http.get<PageData<ImageResourceInfo>>(
`${IMAGES_URL_PREFIX}${pageLink.toQuery()}&includeSystemImages=${includeSystemImages}`,
defaultHttpOptionsFromConfig(config));
}
public getImageInfo(type: ImageResourceType, key: string, config?: RequestConfig): Observable<ImageResourceInfo> {
return this.http.get<ImageResourceInfo>(`${IMAGES_URL_PREFIX}/${type}/${encodeURIComponent(key)}/info`,
defaultHttpOptionsFromConfig(config));
}
public getImageDataUrl(imageUrl: string, preview = false, asString = false, emptyUrl = NO_IMAGE_DATA_URI): Observable<SafeUrl | string> {
const parts = imageUrl.split('/');
const key = parts[parts.length - 1];
parts[parts.length - 1] = encodeURIComponent(key);
const encodedUrl = parts.join('/');
const imageLink = preview ? (encodedUrl + '/preview') : encodedUrl;
return this.loadImageDataUrl(imageLink, asString, emptyUrl);
}
private loadImageDataUrl(imageLink: string, asString = false, emptyUrl = NO_IMAGE_DATA_URI): Observable<SafeUrl | string> {
let request: ReplaySubject<Blob>;
if (this.imagesLoading[imageLink]) {
request = this.imagesLoading[imageLink];
} else {
request = new ReplaySubject<Blob>(1);
this.imagesLoading[imageLink] = request;
const options = defaultHttpOptionsFromConfig({ignoreLoading: true, ignoreErrors: true});
this.http.get(imageLink, {...options, ...{ responseType: 'blob' } }).subscribe({
next: (value) => {
request.next(value);
request.complete();
},
error: err => {
request.error(err);
},
complete: () => {
delete this.imagesLoading[imageLink];
}
});
}
return request.pipe(
switchMap(val => blobToBase64(val).pipe(
map((dataUrl) => asString ? dataUrl : this.sanitizer.bypassSecurityTrustUrl(dataUrl))
)),
catchError(() => of(asString ? emptyUrl : this.sanitizer.bypassSecurityTrustUrl(emptyUrl)))
);
}
public resolveImageUrl(imageUrl: string, preview = false, asString = false, emptyUrl = NO_IMAGE_DATA_URI): Observable<SafeUrl | string> {
imageUrl = removeTbImagePrefix(imageUrl);
if (isImageResourceUrl(imageUrl)) {
return this.getImageDataUrl(imageUrl, preview, asString, emptyUrl);
} else {
return of(asString ? imageUrl : this.sanitizer.bypassSecurityTrustUrl(imageUrl));
}
}
public downloadImage(type: ImageResourceType, key: string): Observable<any> {
return this.resourcesService.downloadResource(`${IMAGES_URL_PREFIX}/${type}/${encodeURIComponent(key)}`);
}
public deleteImage(type: ImageResourceType, key: string, force = false, config?: RequestConfig) {
return this.http.delete(`${IMAGES_URL_PREFIX}/${type}/${encodeURIComponent(key)}?force=${force}`, defaultHttpOptionsFromConfig(config));
}
public exportImage(type: ImageResourceType, key: string, config?: RequestConfig): Observable<ImageExportData> {
return this.http.get<ImageExportData>(`${IMAGES_URL_PREFIX}/${type}/${encodeURIComponent(key)}/export`,
defaultHttpOptionsFromConfig(config));
}
public importImage(imageData: ImageExportData, config?: RequestConfig): Observable<ImageResourceInfo> {
return this.http.put<ImageResourceInfo>('/api/image/import',
imageData, defaultHttpOptionsFromConfig(config));
}
}