From 3e6f101a881cde1e587d2a9253240748a33742cb Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 21 Nov 2023 20:27:24 +0200 Subject: [PATCH] UI: Improve edit image. Improve image gallery grid view. --- ui-ngx/src/app/core/services/menu.service.ts | 7 ++ .../grid/scroll-grid.component.html | 6 +- .../components/grid/scroll-grid.component.ts | 8 ++ .../image/image-dialog.component.html | 102 ++++++++++-------- .../image/image-dialog.component.scss | 32 ++++-- .../image/image-dialog.component.ts | 52 ++++----- .../image/image-gallery.component.html | 98 ++++++++++++++--- .../image/image-gallery.component.scss | 61 +++++++++-- .../image/image-gallery.component.ts | 70 +++++++----- .../datasource/scroll-grid-datasource.ts | 22 ++++ 10 files changed, 325 insertions(+), 133 deletions(-) diff --git a/ui-ngx/src/app/core/services/menu.service.ts b/ui-ngx/src/app/core/services/menu.service.ts index aa622af51b..924d741cf6 100644 --- a/ui-ngx/src/app/core/services/menu.service.ts +++ b/ui-ngx/src/app/core/services/menu.service.ts @@ -534,6 +534,13 @@ export class MenuService { } ] }, + { + id: 'images', + name: 'image.gallery', + type: 'link', + path: '/resources/images', + icon: 'filter' + }, { id: 'resources_library', name: 'resource.resources-library', diff --git a/ui-ngx/src/app/modules/home/components/grid/scroll-grid.component.html b/ui-ngx/src/app/modules/home/components/grid/scroll-grid.component.html index 849dc2e6a9..5d6c066c94 100644 --- a/ui-ngx/src/app/modules/home/components/grid/scroll-grid.component.html +++ b/ui-ngx/src/app/modules/home/components/grid/scroll-grid.component.html @@ -16,14 +16,14 @@ --> - +
-
+
- +
diff --git a/ui-ngx/src/app/modules/home/components/grid/scroll-grid.component.ts b/ui-ngx/src/app/modules/home/components/grid/scroll-grid.component.ts index b2b6e79fe9..dcced7b3c0 100644 --- a/ui-ngx/src/app/modules/home/components/grid/scroll-grid.component.ts +++ b/ui-ngx/src/app/modules/home/components/grid/scroll-grid.component.ts @@ -112,4 +112,12 @@ export class ScrollGridComponent implements OnInit, AfterViewInit, OnChang public update() { this.dataSource.update(); } + + public updateItem(index: number, item: T) { + this.dataSource.updateItem(index, item); + } + + public deleteItem(index: number) { + this.dataSource.deleteItem(index); + } } diff --git a/ui-ngx/src/app/modules/home/components/image/image-dialog.component.html b/ui-ngx/src/app/modules/home/components/image/image-dialog.component.html index 23ef4dcb2a..6526da5392 100644 --- a/ui-ngx/src/app/modules/home/components/image/image-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/image/image-dialog.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+

{{ (readonly ? 'image.image-details' : 'image.edit-image') | translate }}

@@ -30,65 +30,73 @@
- - image.name - - - {{ 'image.name-required' | translate }} - - +
+ + image.name + + + {{ 'image.name-required' | translate }} + + + + +
-
image.image-preview
-
-
- -
-
-
{{ image.descriptor.width }}x{{ image.descriptor.height }}
- -
{{ image.descriptor.size | fileSize }}
-
- - +
+ + +
+
+
+ +
+
+
{{ image.descriptor.width }}x{{ image.descriptor.height }}
+ +
{{ image.descriptor.size | fileSize }}
+
-
- - -
diff --git a/ui-ngx/src/app/modules/home/components/image/image-dialog.component.scss b/ui-ngx/src/app/modules/home/components/image/image-dialog.component.scss index 813974e541..f812ff4fb9 100644 --- a/ui-ngx/src/app/modules/home/components/image/image-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/image/image-dialog.component.scss @@ -14,9 +14,24 @@ * limitations under the License. */ +@import '../../../../../scss/constants'; + :host { + .tb-image-dialog { + @media #{$mat-gt-xs} { + width: 50vh; + } + .mat-mdc-dialog-content { + max-height: 100%; + } + fieldset { + height: 100%; + display: flex; + flex-direction: column; + } + } .tb-image-container { - width: fit-content; + height: 100%; border-radius: 4px; border: 1px solid rgba(0, 0, 0, 0.05); padding: 12px; @@ -26,17 +41,12 @@ gap: 12px; .tb-image-content { + height: 100%; + width: 100%; display: flex; flex-direction: column; align-items: flex-start; gap: 8px; - .tb-image-preview-title { - color: rgba(0, 0, 0, 0.54); - font-size: 13px; - font-style: normal; - font-weight: 500; - line-height: 16px; - } .tb-image-preview-container { position: relative; width: 100%; @@ -49,7 +59,9 @@ justify-content: center; z-index: 1; .tb-image-preview-spacer { - margin-top: 100%; + @media #{$mat-gt-xs} { + margin-top: 100%; + } } .tb-image-preview { position: absolute; @@ -75,6 +87,8 @@ .tb-image-actions { display: flex; align-items: center; + align-self: stretch; + justify-content: space-between; gap: 8px; color: rgba(0,0,0,0.54); } diff --git a/ui-ngx/src/app/modules/home/components/image/image-dialog.component.ts b/ui-ngx/src/app/modules/home/components/image/image-dialog.component.ts index 72b2bbf85c..c248275b99 100644 --- a/ui-ngx/src/app/modules/home/components/image/image-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/image/image-dialog.component.ts @@ -14,19 +14,12 @@ /// limitations under the License. /// -import { ChangeDetectorRef, Component, Inject, OnInit, SkipSelf } from '@angular/core'; +import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; import { ErrorStateMatcher } from '@angular/material/core'; import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { - FormGroupDirective, - NgForm, - UntypedFormBuilder, - UntypedFormControl, - UntypedFormGroup, - Validators -} from '@angular/forms'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { DialogComponent } from '@shared/components/dialog.component'; import { Router } from '@angular/router'; import { ImageService } from '@core/http/image.service'; @@ -45,11 +38,10 @@ export interface ImageDialogData { @Component({ selector: 'tb-image-dialog', templateUrl: './image-dialog.component.html', - providers: [{provide: ErrorStateMatcher, useExisting: ImageDialogComponent}], styleUrls: ['./image-dialog.component.scss'] }) export class ImageDialogComponent extends - DialogComponent implements OnInit, ErrorStateMatcher { + DialogComponent implements OnInit { image: ImageResourceInfo; @@ -57,8 +49,6 @@ export class ImageDialogComponent extends imageFormGroup: UntypedFormGroup; - submitted = false; - imageChanged = false; imagePreviewData: UrlHolder; @@ -68,8 +58,7 @@ export class ImageDialogComponent extends private imageService: ImageService, private dialog: MatDialog, @Inject(MAT_DIALOG_DATA) private data: ImageDialogData, - @SkipSelf() private errorStateMatcher: ErrorStateMatcher, - public dialogRef: MatDialogRef, + public dialogRef: MatDialogRef, public fb: UntypedFormBuilder) { super(store, router, dialogRef); this.image = data.image; @@ -88,14 +77,25 @@ export class ImageDialogComponent extends } } - isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { - const originalErrorState = this.errorStateMatcher.isErrorState(control, form); - const customErrorState = !!(control && control.invalid && this.submitted); - return originalErrorState || customErrorState; + cancel(): void { + this.dialogRef.close(this.imageChanged ? this.image : null); } - cancel(): void { - this.dialogRef.close(this.imageChanged); + revertInfo(): void { + this.imageFormGroup.get('title').setValue(this.image.title); + this.imageFormGroup.markAsPristine(); + } + + saveInfo(): void { + const title: string = this.imageFormGroup.get('title').value; + const image = {...this.image, ...{title}}; + this.imageService.updateImageInfo(image).subscribe( + (saved) => { + this.image = saved; + this.imageChanged = true; + this.imageFormGroup.markAsPristine(); + } + ); } downloadImage($event) { @@ -134,14 +134,4 @@ export class ImageDialogComponent extends }); } - save(): void { - this.submitted = true; - const title: string = this.imageFormGroup.get('title').value; - const image = {...this.image, ...{title}}; - this.imageService.updateImageInfo(image).subscribe( - () => { - this.dialogRef.close(true); - } - ); - } } diff --git a/ui-ngx/src/app/modules/home/components/image/image-gallery.component.html b/ui-ngx/src/app/modules/home/components/image/image-gallery.component.html index 44bf4a4fd5..1f071790be 100644 --- a/ui-ngx/src/app/modules/home/components/image/image-gallery.component.html +++ b/ui-ngx/src/app/modules/home/components/image/image-gallery.component.html @@ -19,7 +19,7 @@
- image.gallery + image.gallery
@@ -43,7 +43,7 @@
-
+
+
-
+
+ + + + + + + +
@@ -243,9 +286,40 @@
image.no-images
- -
+ +
+
+
+ + + +
+
+
+ +
{{ item.title }}
diff --git a/ui-ngx/src/app/modules/home/components/image/image-gallery.component.scss b/ui-ngx/src/app/modules/home/components/image/image-gallery.component.scss index fe48ba197f..0fe15f8e3e 100644 --- a/ui-ngx/src/app/modules/home/components/image/image-gallery.component.scss +++ b/ui-ngx/src/app/modules/home/components/image/image-gallery.component.scss @@ -77,17 +77,70 @@ $tb-button-selected-color: rgb(255, 110, 64) !default; } .tb-image-card { + position: relative; height: 100%; - border-radius: 8px; + border-radius: 4px; + box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.24); padding: 8px; display: flex; gap: 8px; flex-direction: column; cursor: pointer; - transition: box-shadow 0.2s; + + .tb-image-card-overlay { + position: absolute; + pointer-events: none; + inset: 0; + border-radius: 4px; + z-index: 2; + display: flex; + flex-direction: column; + .tb-image-card-overlay-buttons { + width: 100%; + display: flex; + justify-content: flex-end; + align-items: center; + color: rgba(0,0,0,0.78); + opacity: 0; + transition: opacity 0.5s; + } + } + + &:hover { + .tb-image-card-overlay { + .tb-image-card-overlay-buttons { + opacity: 1; + } + } + .tb-image-preview-container { + .tb-image-preview-overlay { + background: rgba(245,245,245,0.6); + backdrop-filter: blur(4px); + .mdc-button { + opacity: 1; + } + } + } + } .tb-image-preview-container { position: relative; + .tb-image-preview-overlay { + position: absolute; + inset: 0; + z-index: 1; + background: rgba(245,245,245,0); + backdrop-filter: none; + transition: all 0.5s; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + .mdc-button { + opacity: 0; + transition: opacity 0.5s; + } + } .tb-image-preview-spacer { margin-top: 100%; } @@ -152,10 +205,6 @@ $tb-button-selected-color: rgb(255, 110, 64) !default; } } } - - &:hover { - box-shadow: 0 0 28px 0 rgba(0, 0, 0, 0.24); - } } .table-container { diff --git a/ui-ngx/src/app/modules/home/components/image/image-gallery.component.ts b/ui-ngx/src/app/modules/home/components/image/image-gallery.component.ts index 2f0ee673bd..7e8cae6719 100644 --- a/ui-ngx/src/app/modules/home/components/image/image-gallery.component.ts +++ b/ui-ngx/src/app/modules/home/components/image/image-gallery.component.ts @@ -21,7 +21,7 @@ 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 { catchError, debounceTime, distinctUntilChanged, map, skip, take, takeUntil, tap } from 'rxjs/operators'; import { AfterViewInit, ChangeDetectorRef, @@ -38,7 +38,6 @@ 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'; @@ -52,19 +51,14 @@ 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, UploadImageDialogData } from '@home/components/image/upload-image-dialog.component'; import { ImageDialogComponent, ImageDialogData } from '@home/components/image/image-dialog.component'; +import { EntityBooleanFunction } from '@home/models/entity/entities-table-config.models'; @Component({ selector: 'tb-image-gallery', @@ -178,7 +172,7 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe } } if (this.mode === 'list') { - this.dataSource = new ImagesDatasource(this.imageService); + this.dataSource = new ImagesDatasource(this.imageService, entity => !this.readonly(entity)); } } @@ -234,7 +228,7 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe } this.mode = targetMode; if (this.mode === 'list') { - this.dataSource = new ImagesDatasource(this.imageService); + this.dataSource = new ImagesDatasource(this.imageService, entity => !this.readonly(entity)); } setTimeout(() => { this.updateMode(); @@ -341,6 +335,22 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe } } + 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); + } + } + enterFilterMode() { this.textSearchMode = true; setTimeout(() => { @@ -366,7 +376,7 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe return this.authUser.authority !== Authority.SYS_ADMIN && this.isSystem(image); } - deleteImage($event: Event, image: ImageResourceInfo) { + deleteImage($event: Event, image: ImageResourceInfo, itemIndex = -1) { if ($event) { $event.stopPropagation(); } @@ -378,7 +388,7 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe if (result) { this.imageService.deleteImage(imageResourceType(image), image.resourceKey).subscribe( () => { - this.updateData(); + this.imageDeleted(itemIndex); } ); } @@ -440,12 +450,12 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe }); } - editImage($event: Event, image: ImageResourceInfo) { + editImage($event: Event, image: ImageResourceInfo, itemIndex = -1) { if ($event) { $event.stopPropagation(); } this.dialog.open(ImageDialogComponent, { + ImageResourceInfo>(ImageDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { @@ -454,7 +464,7 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe } }).afterClosed().subscribe((result) => { if (result) { - this.updateData(); + this.imageUpdated(result, itemIndex); } }); } @@ -486,7 +496,8 @@ class ImagesDatasource implements DataSource { public dataLoading = true; - constructor(private imageService: ImageService) { + constructor(private imageService: ImageService, + protected selectionEnabledFunction: EntityBooleanFunction) { } connect(collectionViewer: CollectionViewer): @@ -548,15 +559,24 @@ class ImagesDatasource implements DataSource { } 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); - }); - } + this.entitiesSubject.pipe( + tap((entities) => { + const numSelected = this.selection.selected.length; + if (numSelected === this.selectableEntitiesCount(entities)) { + this.selection.clear(); + } else { + entities.forEach(row => { + if (this.selectionEnabledFunction(row)) { + this.selection.select(row); + } + }); + } + }), + take(1) + ).subscribe(); } + private selectableEntitiesCount(entities: Array): number { + return entities.filter((entity) => this.selectionEnabledFunction(entity)).length; + } } diff --git a/ui-ngx/src/app/modules/home/models/datasource/scroll-grid-datasource.ts b/ui-ngx/src/app/modules/home/models/datasource/scroll-grid-datasource.ts index 83087eadc0..1298ec4f8f 100644 --- a/ui-ngx/src/app/modules/home/models/datasource/scroll-grid-datasource.ts +++ b/ui-ngx/src/app/modules/home/models/datasource/scroll-grid-datasource.ts @@ -88,6 +88,10 @@ export class ScrollGridDatasource extends DataSource<(T | GridCellType)[]> return !!this._subscription && !this._subscription.closed; } + get currentColumns(): number { + return this._columns; + } + public updateFilter(filter: F) { this.filter = filter; this.update(); @@ -123,6 +127,24 @@ export class ScrollGridDatasource extends DataSource<(T | GridCellType)[]> } } + public updateItem(index: number, item: T) { + this._data[index] = item; + this._dataUpdated(); + } + + public deleteItem(index: number) { + if (index < this._data.length) { + this._data.splice(index, 1); + this._totalElements--; + const rowsLength = this._totalElements ? Math.ceil(this._totalElements / this._columns) : 100000; + this._rows = Array.from({length: rowsLength}); + this._dataUpdated(); + if (this._hasNext) { + this._fetchDataFromRange(this._viewport.getRenderedRange()); + } + } + } + private _detectColumns(): number { if (typeof this.columns !== 'object') { return this.columns;