15 changed files with 1310 additions and 18 deletions
@ -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); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue