2023-11-21 11:27:37 +02:00
|
|
|
///
|
|
|
|
|
/// 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.
|
|
|
|
|
///
|
|
|
|
|
|
2023-11-23 17:56:04 +02:00
|
|
|
import {
|
|
|
|
|
ImageResourceInfo,
|
|
|
|
|
ImageResourceInfoWithReferences,
|
|
|
|
|
imageResourceType,
|
|
|
|
|
toImageDeleteResult
|
|
|
|
|
} from '@shared/models/resource.models';
|
|
|
|
|
import { forkJoin, merge, Observable, of, Subject, Subscription } from 'rxjs';
|
2023-11-21 11:27:37 +02:00
|
|
|
import { ImageService } from '@core/http/image.service';
|
|
|
|
|
import { TranslateService } from '@ngx-translate/core';
|
|
|
|
|
import { PageLink, PageQueryParam } from '@shared/models/page/page-link';
|
2023-11-23 17:56:04 +02:00
|
|
|
import { catchError, debounceTime, distinctUntilChanged, map, skip, takeUntil } from 'rxjs/operators';
|
2023-11-21 11:27:37 +02:00
|
|
|
import {
|
|
|
|
|
AfterViewInit,
|
|
|
|
|
ChangeDetectorRef,
|
|
|
|
|
Component,
|
2023-11-24 18:47:46 +02:00
|
|
|
ElementRef, EventEmitter,
|
2023-11-21 11:27:37 +02:00
|
|
|
Input,
|
|
|
|
|
OnDestroy,
|
2023-11-24 18:47:46 +02:00
|
|
|
OnInit, Output,
|
2023-11-21 11:27:37 +02:00
|
|
|
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';
|
2023-11-23 17:56:04 +02:00
|
|
|
import { isEqual, isNotEmptyStr, parseHttpErrorMessage } from '@core/utils';
|
2023-11-21 11:27:37 +02:00
|
|
|
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';
|
2023-11-22 14:49:46 +02:00
|
|
|
import { ItemSizeStrategy, ScrollGridComponent } from '@home/components/grid/scroll-grid.component';
|
2023-11-21 11:27:37 +02:00
|
|
|
import { MatDialog } from '@angular/material/dialog';
|
2023-11-21 14:08:52 +02:00
|
|
|
import {
|
|
|
|
|
UploadImageDialogComponent,
|
|
|
|
|
UploadImageDialogData
|
|
|
|
|
} from '@home/components/image/upload-image-dialog.component';
|
|
|
|
|
import { ImageDialogComponent, ImageDialogData } from '@home/components/image/image-dialog.component';
|
2023-11-23 17:56:04 +02:00
|
|
|
import { ImportExportService } from '@home/components/import-export/import-export.service';
|
|
|
|
|
import { ActionNotificationShow } from '@core/notification/notification.actions';
|
|
|
|
|
import {
|
|
|
|
|
ImagesInUseDialogComponent,
|
|
|
|
|
ImagesInUseDialogData
|
|
|
|
|
} from '@home/components/image/images-in-use-dialog.component';
|
|
|
|
|
import { ImagesDatasource } from '@home/components/image/images-datasource';
|
2023-11-21 11:27:37 +02:00
|
|
|
|
2023-11-23 19:34:31 +02:00
|
|
|
interface GridImagesFilter {
|
|
|
|
|
search: string;
|
|
|
|
|
includeSystemImages: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-24 18:47:46 +02:00
|
|
|
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 popoverGridColumns: ScrollGridColumns = {
|
2023-11-27 14:56:22 +02:00
|
|
|
columns: 1,
|
2023-11-24 18:47:46 +02:00
|
|
|
breakpoints: {
|
2023-11-27 14:56:22 +02:00
|
|
|
'screen and (min-width: 2320px)': 8,
|
|
|
|
|
'gt-lg': 6,
|
|
|
|
|
'screen and (min-width: 1600px)': 5,
|
|
|
|
|
'gt-md': 4,
|
|
|
|
|
'gt-sm': 3,
|
|
|
|
|
'gt-xs': 2
|
2023-11-24 18:47:46 +02:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2023-11-21 11:27:37 +02:00
|
|
|
@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;
|
|
|
|
|
|
2023-11-24 18:47:46 +02:00
|
|
|
@Input()
|
|
|
|
|
@coerceBoolean()
|
|
|
|
|
popoverMode = false;
|
|
|
|
|
|
2023-11-21 11:27:37 +02:00
|
|
|
@Input()
|
|
|
|
|
mode: 'list' | 'grid' = 'list';
|
|
|
|
|
|
2023-11-24 18:47:46 +02:00
|
|
|
@Input()
|
|
|
|
|
selectionMode = false;
|
|
|
|
|
|
|
|
|
|
@Output()
|
|
|
|
|
imageSelected = new EventEmitter<ImageResourceInfo>();
|
|
|
|
|
|
2023-11-21 11:27:37 +02:00
|
|
|
@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});
|
2023-11-27 14:56:22 +02:00
|
|
|
includeSystemImages = false;
|
2023-11-21 11:27:37 +02:00
|
|
|
|
2023-11-24 18:47:46 +02:00
|
|
|
gridColumns: ScrollGridColumns;
|
2023-11-21 11:27:37 +02:00
|
|
|
|
2023-11-23 19:34:31 +02:00
|
|
|
gridImagesFetchFunction: GridEntitiesFetchFunction<ImageResourceInfo, GridImagesFilter>;
|
|
|
|
|
gridImagesFilter: GridImagesFilter = {
|
|
|
|
|
search: '',
|
|
|
|
|
includeSystemImages: false
|
|
|
|
|
};
|
2023-11-21 11:27:37 +02:00
|
|
|
|
2023-11-22 14:49:46 +02:00
|
|
|
gridImagesItemSizeStrategy: ItemSizeStrategy = {
|
|
|
|
|
defaultItemSize: 200,
|
|
|
|
|
itemSizeFunction: itemWidth => itemWidth + 72
|
|
|
|
|
};
|
|
|
|
|
|
2023-11-21 11:27:37 +02:00
|
|
|
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,
|
2023-11-23 17:56:04 +02:00
|
|
|
private importExportService: ImportExportService,
|
2023-11-21 11:27:37 +02:00
|
|
|
private elementRef: ElementRef,
|
|
|
|
|
private cd: ChangeDetectorRef,
|
|
|
|
|
private fb: FormBuilder) {
|
|
|
|
|
super(store);
|
|
|
|
|
|
|
|
|
|
this.gridImagesFetchFunction = (pageSize, page, filter) => {
|
2023-11-23 19:34:31 +02:00
|
|
|
const pageLink = new PageLink(pageSize, page, filter.search, {
|
2023-11-21 11:27:37 +02:00
|
|
|
property: 'createdTime',
|
|
|
|
|
direction: Direction.DESC
|
|
|
|
|
});
|
2023-11-23 19:34:31 +02:00
|
|
|
return this.imageService.getImages(pageLink, filter.includeSystemImages);
|
2023-11-21 11:27:37 +02:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ngOnInit(): void {
|
2023-11-24 18:47:46 +02:00
|
|
|
this.gridColumns = this.popoverMode ? popoverGridColumns : pageGridColumns;
|
2023-11-23 19:34:31 +02:00
|
|
|
this.displayedColumns = this.computeDisplayedColumns();
|
2023-11-21 11:27:37 +02:00
|
|
|
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') {
|
2023-11-23 17:56:04 +02:00
|
|
|
this.dataSource = new ImagesDatasource(this.imageService, null,
|
|
|
|
|
entity => this.deleteEnabled(entity));
|
2023-11-21 11:27:37 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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) =>
|
2023-11-23 19:34:31 +02:00
|
|
|
((this.mode === 'list' ? this.pageLink.textSearch : this.gridImagesFilter.search) ?? '') === current.trim()),
|
2023-11-21 11:27:37 +02:00
|
|
|
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 {
|
2023-11-23 19:34:31 +02:00
|
|
|
this.gridImagesFilter = {
|
|
|
|
|
search: isNotEmptyStr(value) ? encodeURI(value) : null,
|
2023-11-27 14:56:22 +02:00
|
|
|
includeSystemImages: this.includeSystemImages
|
2023-11-23 19:34:31 +02:00
|
|
|
};
|
|
|
|
|
this.cd.markForCheck();
|
|
|
|
|
}
|
|
|
|
|
});
|
2023-11-21 11:27:37 +02:00
|
|
|
this.updateMode();
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-27 14:56:22 +02:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-21 11:27:37 +02:00
|
|
|
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') {
|
2023-11-23 17:56:04 +02:00
|
|
|
this.dataSource = new ImagesDatasource(this.imageService, null,
|
|
|
|
|
entity => this.deleteEnabled(entity));
|
2023-11-21 11:27:37 +02:00
|
|
|
}
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
this.updateMode();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-23 19:34:31 +02:00
|
|
|
public get isSysAdmin(): boolean {
|
|
|
|
|
return this.authUser.authority === Authority.SYS_ADMIN;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private computeDisplayedColumns(): string[] {
|
2023-11-27 14:56:22 +02:00
|
|
|
let columns: string[];
|
2023-11-24 18:47:46 +02:00
|
|
|
if (this.selectionMode) {
|
2023-11-27 14:56:22 +02:00
|
|
|
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');
|
2023-11-23 19:34:31 +02:00
|
|
|
}
|
|
|
|
|
return columns;
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-21 11:27:37 +02:00
|
|
|
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;
|
|
|
|
|
}
|
2023-11-27 14:56:22 +02:00
|
|
|
this.dataSource.loadEntities(this.pageLink, this.includeSystemImages);
|
2023-11-21 11:27:37 +02:00
|
|
|
} else {
|
|
|
|
|
this.gridComponent.update();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-21 20:27:24 +02:00
|
|
|
private imageUpdated(image: ImageResourceInfo, index = -1) {
|
|
|
|
|
if (this.mode === 'list') {
|
|
|
|
|
this.updateData();
|
|
|
|
|
} else {
|
|
|
|
|
this.gridComponent.updateItem(index, image);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private imageDeleted(index = -1) {
|
|
|
|
|
if (this.mode === 'list') {
|
|
|
|
|
this.updateData();
|
|
|
|
|
} else {
|
|
|
|
|
this.gridComponent.deleteItem(index);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-21 11:27:37 +02:00
|
|
|
enterFilterMode() {
|
|
|
|
|
this.textSearchMode = true;
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
this.searchInputField.nativeElement.focus();
|
|
|
|
|
this.searchInputField.nativeElement.setSelectionRange(0, 0);
|
|
|
|
|
}, 10);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
exitFilterMode() {
|
|
|
|
|
this.textSearchMode = false;
|
|
|
|
|
this.textSearch.reset();
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-21 14:08:52 +02:00
|
|
|
trackByEntity(index: number, entity: BaseData<HasId>) {
|
|
|
|
|
return entity;
|
2023-11-21 11:27:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isSystem(image?: ImageResourceInfo): boolean {
|
2023-11-23 19:34:31 +02:00
|
|
|
return !this.isSysAdmin && image?.tenantId?.id === NULL_UUID;
|
2023-11-21 11:27:37 +02:00
|
|
|
}
|
|
|
|
|
|
2023-11-21 14:08:52 +02:00
|
|
|
readonly(image?: ImageResourceInfo): boolean {
|
|
|
|
|
return this.authUser.authority !== Authority.SYS_ADMIN && this.isSystem(image);
|
2023-11-21 11:27:37 +02:00
|
|
|
}
|
|
|
|
|
|
2023-11-23 17:56:04 +02:00
|
|
|
deleteEnabled(image?: ImageResourceInfo): boolean {
|
|
|
|
|
return this.authUser.authority === Authority.SYS_ADMIN || !this.isSystem(image);
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-21 20:27:24 +02:00
|
|
|
deleteImage($event: Event, image: ImageResourceInfo, itemIndex = -1) {
|
2023-11-21 11:27:37 +02:00
|
|
|
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) {
|
2023-11-23 17:56:04 +02:00
|
|
|
this.imageService.deleteImage(imageResourceType(image), image.resourceKey, false, {ignoreErrors: true}).pipe(
|
|
|
|
|
map(() => toImageDeleteResult(image)),
|
|
|
|
|
catchError((err) => of(toImageDeleteResult(image, err)))
|
|
|
|
|
).subscribe(
|
|
|
|
|
(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);
|
|
|
|
|
}
|
|
|
|
|
});
|
2023-11-21 11:27:37 +02:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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) =>
|
2023-11-23 17:56:04 +02:00
|
|
|
this.imageService.deleteImage(imageResourceType(image), image.resourceKey, false, {ignoreErrors: true}).pipe(
|
|
|
|
|
map(() => toImageDeleteResult(image)),
|
|
|
|
|
catchError((err) => of(toImageDeleteResult(image, err)))
|
|
|
|
|
)
|
|
|
|
|
);
|
2023-11-21 11:27:37 +02:00
|
|
|
forkJoin(tasks).subscribe(
|
2023-11-23 17:56:04 +02:00
|
|
|
(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);
|
|
|
|
|
}
|
2023-11-21 11:27:37 +02:00
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
downloadImage($event, image: ImageResourceInfo) {
|
|
|
|
|
if ($event) {
|
|
|
|
|
$event.stopPropagation();
|
|
|
|
|
}
|
|
|
|
|
this.imageService.downloadImage(imageResourceType(image), image.resourceKey).subscribe();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
exportImage($event, image: ImageResourceInfo) {
|
|
|
|
|
if ($event) {
|
|
|
|
|
$event.stopPropagation();
|
|
|
|
|
}
|
2023-11-23 17:56:04 +02:00
|
|
|
this.importExportService.exportImage(imageResourceType(image), image.resourceKey);
|
2023-11-21 11:27:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
importImage(): void {
|
2023-11-23 17:56:04 +02:00
|
|
|
this.importExportService.importImage().subscribe((image) => {
|
|
|
|
|
if (image) {
|
|
|
|
|
this.updateData();
|
|
|
|
|
}
|
|
|
|
|
});
|
2023-11-21 11:27:37 +02:00
|
|
|
}
|
|
|
|
|
|
2023-11-24 18:47:46 +02:00
|
|
|
selectImage($event, image: ImageResourceInfo) {
|
|
|
|
|
if ($event) {
|
|
|
|
|
$event.stopPropagation();
|
|
|
|
|
}
|
|
|
|
|
this.imageSelected.next(image);
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-27 14:56:22 +02:00
|
|
|
rowClick($event, image: ImageResourceInfo) {
|
|
|
|
|
if (this.selectionMode) {
|
|
|
|
|
this.selectImage($event, image);
|
|
|
|
|
} else {
|
|
|
|
|
this.dataSource.selection.toggle(image);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-21 11:27:37 +02:00
|
|
|
uploadImage(): void {
|
2023-11-21 14:08:52 +02:00
|
|
|
this.dialog.open<UploadImageDialogComponent, UploadImageDialogData,
|
|
|
|
|
ImageResourceInfo>(UploadImageDialogComponent, {
|
2023-11-21 11:27:37 +02:00
|
|
|
disableClose: true,
|
2023-11-21 14:08:52 +02:00
|
|
|
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
|
|
|
|
|
data: {}
|
2023-11-21 11:27:37 +02:00
|
|
|
}).afterClosed().subscribe((result) => {
|
|
|
|
|
if (result) {
|
|
|
|
|
this.updateData();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-21 20:27:24 +02:00
|
|
|
editImage($event: Event, image: ImageResourceInfo, itemIndex = -1) {
|
2023-11-21 11:27:37 +02:00
|
|
|
if ($event) {
|
|
|
|
|
$event.stopPropagation();
|
|
|
|
|
}
|
2023-11-21 14:08:52 +02:00
|
|
|
this.dialog.open<ImageDialogComponent, ImageDialogData,
|
2023-11-21 20:27:24 +02:00
|
|
|
ImageResourceInfo>(ImageDialogComponent, {
|
2023-11-21 14:08:52 +02:00
|
|
|
disableClose: true,
|
|
|
|
|
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
|
|
|
|
|
data: {
|
|
|
|
|
image,
|
|
|
|
|
readonly: this.readonly(image)
|
|
|
|
|
}
|
|
|
|
|
}).afterClosed().subscribe((result) => {
|
|
|
|
|
if (result) {
|
2023-11-21 20:27:24 +02:00
|
|
|
this.imageUpdated(result, itemIndex);
|
2023-11-21 14:08:52 +02:00
|
|
|
}
|
|
|
|
|
});
|
2023-11-21 11:27:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|