17 changed files with 915 additions and 14 deletions
@ -0,0 +1,46 @@ |
|||||
|
///
|
||||
|
/// 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 { Injectable } from '@angular/core'; |
||||
|
import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; |
||||
|
import { Observable } from 'rxjs'; |
||||
|
import { HttpClient } from '@angular/common/http'; |
||||
|
import { PageLink } from '@shared/models/page/page-link'; |
||||
|
import { PageData } from '@shared/models/page/page-data'; |
||||
|
import { AlarmComment, AlarmCommentInfo } from '@shared/models/alarm.models'; |
||||
|
|
||||
|
@Injectable({ |
||||
|
providedIn: 'root' |
||||
|
}) |
||||
|
export class AlarmCommentService { |
||||
|
|
||||
|
constructor( |
||||
|
private http: HttpClient |
||||
|
) { } |
||||
|
|
||||
|
public saveAlarmComment(alarmId: string, alarmComment: AlarmComment, config?: RequestConfig): Observable<AlarmComment> { |
||||
|
return this.http.post<AlarmComment>(`/api/alarm/${alarmId}/comment`, alarmComment, defaultHttpOptionsFromConfig(config)); |
||||
|
} |
||||
|
|
||||
|
public getAlarmComments(alarmId: string, pageLink: PageLink, config?: RequestConfig): Observable<PageData<AlarmCommentInfo>> { |
||||
|
return this.http.get<PageData<AlarmComment>>(`/api/alarm/${alarmId}/comment${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); |
||||
|
} |
||||
|
|
||||
|
public deleteAlarmComments(alarmId: string, commentId: string, config?: RequestConfig): Observable<void> { |
||||
|
return this.http.delete<void>(`/api/alarm/${alarmId}/comment/${commentId}`, defaultHttpOptionsFromConfig(config)); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,55 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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 style="min-width: 600px;"> |
||||
|
<mat-toolbar color="primary"> |
||||
|
<h2>{{ 'alarm.comments' | translate }}</h2> |
||||
|
<span fxFlex></span> |
||||
|
<button mat-icon-button |
||||
|
(click)="close()" |
||||
|
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 style="padding: 0"> |
||||
|
<tb-alarm-comment #alarmCommentComponent [alarmId]="alarmId" |
||||
|
[commentsHeaderEnabled]="commentsHeaderEnabled"> |
||||
|
</tb-alarm-comment> |
||||
|
</div> |
||||
|
<div mat-dialog-actions fxLayout="row"> |
||||
|
<button mat-button color="primary" |
||||
|
type="button" |
||||
|
[disabled]="(isLoading$ | async)" |
||||
|
(click)="close()" cdkFocusInitial> |
||||
|
{{ 'action.close' | translate }} |
||||
|
</button> |
||||
|
<span fxFlex></span> |
||||
|
<button mat-raised-button color="primary" |
||||
|
type="button" |
||||
|
(click)="changeSortDirection()"> |
||||
|
{{ 'alarm-comment.sort-direction' | translate}} |
||||
|
</button> |
||||
|
<button mat-raised-button color="primary" |
||||
|
type="button" |
||||
|
(click)="refresh()"> |
||||
|
{{ 'alarm-comment.refresh' | translate}} |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,64 @@ |
|||||
|
///
|
||||
|
/// 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, Inject, ViewChild } from '@angular/core'; |
||||
|
import { DialogComponent } from '@shared/components/dialog.component'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; |
||||
|
import { Router } from '@angular/router'; |
||||
|
import { AlarmInfo } from '@shared/models/alarm.models'; |
||||
|
import { AlarmCommentComponent } from '@home/components/alarm/alarm-comment.component'; |
||||
|
|
||||
|
export interface AlarmCommentDialogData { |
||||
|
alarmId?: string; |
||||
|
alarm?: AlarmInfo; |
||||
|
commentsHeaderEnabled: boolean; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-alarm-comment-dialog', |
||||
|
templateUrl: './alarm-comment-dialog.component.html', |
||||
|
styleUrls: [] |
||||
|
}) |
||||
|
export class AlarmCommentDialogComponent extends DialogComponent<AlarmCommentDialogComponent, void> { |
||||
|
|
||||
|
alarmId: string; |
||||
|
commentsHeaderEnabled: boolean = false; |
||||
|
|
||||
|
@ViewChild('alarmCommentComponent', { static: true }) alarmCommentComponent: AlarmCommentComponent; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
protected router: Router, |
||||
|
@Inject(MAT_DIALOG_DATA) public data: AlarmCommentDialogData, |
||||
|
public dialogRef: MatDialogRef<AlarmCommentDialogComponent, void>) { |
||||
|
super(store, router, dialogRef); |
||||
|
this.commentsHeaderEnabled = this.data.commentsHeaderEnabled |
||||
|
this.alarmId = this.data.alarmId; |
||||
|
} |
||||
|
|
||||
|
changeSortDirection() { |
||||
|
this.alarmCommentComponent.changeSortDirection(); |
||||
|
} |
||||
|
|
||||
|
refresh() { |
||||
|
this.alarmCommentComponent.loadAlarmComments(); |
||||
|
} |
||||
|
|
||||
|
close(): void { |
||||
|
this.dialogRef.close(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,161 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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 fxFlexFill [formGroup]="alarmCommentFormGroup" class="tb-alarm-comments" fxLayout="column"> |
||||
|
<div *ngIf="commentsHeaderEnabled" class="tb-alarm-comments-header" fxLayout="row" fxLayoutAlign="space-between baseline"> |
||||
|
<span class="tb-alarm-comments-header-title">{{ 'alarm-comment.comments' | translate }}</span> |
||||
|
<div> |
||||
|
<button mat-icon-button |
||||
|
type="button" |
||||
|
(click)="changeSortDirection()" |
||||
|
matTooltip="{{ 'alarm-comment.sort-direction' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon class="material-icons">{{ getSortDirectionIcon() }}</mat-icon> |
||||
|
</button> |
||||
|
<button mat-icon-button |
||||
|
type="button" |
||||
|
(click)="loadAlarmComments()" |
||||
|
matTooltip="{{ 'alarm-comment.refresh' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon class="material-icons">refresh</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div fxLayout="column" fxLayoutGap="32px"> |
||||
|
<ng-container *ngIf="isDirectionDescending()"> |
||||
|
<ng-container *ngTemplateOutlet="commentInput"></ng-container> |
||||
|
</ng-container> |
||||
|
<div fxFlex *ngFor="let displayDataElement of displayData; let i = index"> |
||||
|
<div style="margin-left: 38px" |
||||
|
*ngIf="displayDataElement.isSystemComment; else userComment"> |
||||
|
<span class="tb-alarm-comments-system-text" |
||||
|
style="margin-right: 8px"> |
||||
|
{{ displayDataElement.commentText }} |
||||
|
</span> |
||||
|
<span class="tb-alarm-comments-time"> |
||||
|
{{ displayDataElement.createdDateAgo }} |
||||
|
</span> |
||||
|
</div> |
||||
|
<ng-template #userComment> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" |
||||
|
*ngIf="!displayDataElement.edit; else commentEditing" |
||||
|
(mouseenter)="onCommentMouseEnter(displayDataElement.commentId, i)" |
||||
|
(mouseleave)="onCommentMouseLeave(i)"> |
||||
|
<div fxLayout="row" fxLayoutAlign="center center" fxFlexAlign="start" |
||||
|
class="tb-alarm-comments-user-avatar" |
||||
|
[style.background-color]="displayDataElement.avatarBgColor"> |
||||
|
{{ getUserInitials(displayDataElement.displayName) }} |
||||
|
</div> |
||||
|
<div fxFlex fxLayout="column" fxLayoutGap="5px"> |
||||
|
<div fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center"> |
||||
|
<span class="tb-alarm-comments-user-name">{{ displayDataElement.displayName }}</span> |
||||
|
<span class="tb-alarm-comments-time" *ngIf="displayDataElement.isEdited"> |
||||
|
edited {{ displayDataElement.editedDateAgo }} |
||||
|
</span> |
||||
|
<span class="tb-alarm-comments-time" *ngIf="!displayDataElement.isEdited"> |
||||
|
{{ displayDataElement.createdDateAgo }} |
||||
|
</span> |
||||
|
</div> |
||||
|
<span class="tb-alarm-comments-text">{{ displayDataElement.commentText }}</span> |
||||
|
</div> |
||||
|
<div fxLayout="row" class="tb-alarm-comments-action-buttons" |
||||
|
[ngClass]="{ 'show-buttons': displayDataElement.showActions }"> |
||||
|
<button mat-button mat-icon-button |
||||
|
type="button" |
||||
|
(click)="editComment(displayDataElement.commentId)"> |
||||
|
<mat-icon class="material-icons">edit</mat-icon> |
||||
|
</button> |
||||
|
<button mat-button mat-icon-button |
||||
|
type="button" |
||||
|
(click)="deleteComment(displayDataElement.commentId)"> |
||||
|
<mat-icon class="material-icons">delete</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
<ng-template #commentEditing> |
||||
|
<div fxLayoutAlign="row center" fxLayoutGap="8px"> |
||||
|
<div fxLayout="row" fxLayoutAlign="center center" |
||||
|
class="tb-alarm-comments-user-avatar" |
||||
|
[style.background-color]="displayDataElement.avatarBgColor"> |
||||
|
{{ getUserInitials(displayDataElement.displayName) }} |
||||
|
</div> |
||||
|
<mat-form-field fxFlex appearance="standard" class="mat-block"> |
||||
|
<textarea matInput |
||||
|
type="text" |
||||
|
placeholder="{{ 'alarm-comment.add' | translate }}" |
||||
|
cdkTextareaAutosize |
||||
|
cdkAutosizeMinRows="1" |
||||
|
cols="1" |
||||
|
formControlName="alarmCommentEdit" |
||||
|
(keyup.enter)="saveEditedComment(displayDataElement.commentId)" |
||||
|
(keydown.enter)="$event.preventDefault()"> |
||||
|
</textarea> |
||||
|
<div matSuffix fxLayout="row"> |
||||
|
<button mat-icon-button |
||||
|
(click)="cancelEdit(displayDataElement.commentId)" |
||||
|
type="button"> |
||||
|
<mat-icon class="material-icons red-button">close</mat-icon> |
||||
|
</button> |
||||
|
<button mat-icon-button |
||||
|
type="button" |
||||
|
(click)="saveEditedComment(displayDataElement.commentId)"> |
||||
|
<mat-icon class="material-icons green-button">check</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
</ng-template> |
||||
|
</div> |
||||
|
<ng-container *ngIf="isDirectionAscending()"> |
||||
|
<ng-container *ngTemplateOutlet="commentInput"></ng-container> |
||||
|
</ng-container> |
||||
|
</div> |
||||
|
<ng-template #commentInput> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" fxFlex> |
||||
|
<div fxLayout="row" fxLayoutAlign="center center" |
||||
|
class="tb-alarm-comments-user-avatar" |
||||
|
*ngIf="userDisplayName$ | async; let userDisplayName" |
||||
|
[style.background-color]="getCurrentUserBgColor(userDisplayName)"> |
||||
|
{{ getUserInitials(userDisplayName) }} |
||||
|
</div> |
||||
|
<mat-form-field appearance="standard" fxFlex class="mat-block"> |
||||
|
<textarea matInput |
||||
|
type="text" |
||||
|
placeholder="{{ 'alarm-comment.add' | translate }}" |
||||
|
cdkTextareaAutosize |
||||
|
cdkAutosizeMinRows="1" |
||||
|
cols="1" |
||||
|
formControlName="alarmComment" |
||||
|
(keyup.enter)="saveComment()" |
||||
|
(keydown.enter)="$event.preventDefault()"> |
||||
|
</textarea> |
||||
|
<button mat-button |
||||
|
mat-icon-button |
||||
|
type="button" |
||||
|
matSuffix |
||||
|
*ngIf="getAlarmCommentFormControl().value" |
||||
|
(click)="saveComment()"> |
||||
|
<mat-icon color="primary"> |
||||
|
send |
||||
|
</mat-icon> |
||||
|
</button> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
</div> |
||||
@ -0,0 +1,107 @@ |
|||||
|
/** |
||||
|
* 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. |
||||
|
*/ |
||||
|
:host { |
||||
|
.tb-alarm-comments { |
||||
|
padding: 16px 24px 24px 24px; |
||||
|
background-color: #fafafa; |
||||
|
max-width: 600px; |
||||
|
|
||||
|
&-header { |
||||
|
background-color: #fafafa; |
||||
|
position: sticky; |
||||
|
top: -25px; |
||||
|
z-index: 1; |
||||
|
margin-bottom: 10px; |
||||
|
|
||||
|
&-title { |
||||
|
color: rgba(0, 0, 0, 0.76); |
||||
|
letter-spacing: 0.25px; |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
|
||||
|
.mat-icon { |
||||
|
color: rgba(0, 0, 0, 0.38); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&-user-avatar { |
||||
|
width: 28px; |
||||
|
min-width: 28px; |
||||
|
height: 28px; |
||||
|
min-height: 28px; |
||||
|
border-radius: 50%; |
||||
|
font-weight: 700; |
||||
|
color: #FFFFFF; |
||||
|
font-size: 13px; |
||||
|
} |
||||
|
|
||||
|
&-user-name { |
||||
|
font-size: 16px; |
||||
|
color: rgba(0, 0, 0, 0.76); |
||||
|
font-weight: 500; |
||||
|
letter-spacing: 0.25px; |
||||
|
} |
||||
|
|
||||
|
&-time { |
||||
|
font-size: 14px; |
||||
|
font-weight: 400; |
||||
|
color: rgba(0, 0, 0, 0.38); |
||||
|
letter-spacing: 0.2px |
||||
|
} |
||||
|
|
||||
|
&-system-text { |
||||
|
color: rgba(0, 0, 0, 0.38); |
||||
|
font-weight: 500; |
||||
|
letter-spacing: 0.25px; |
||||
|
} |
||||
|
|
||||
|
&-text { |
||||
|
white-space: pre-line; |
||||
|
word-break: break-word; |
||||
|
color: rgba(0, 0, 0, 0.54); |
||||
|
letter-spacing: 0.15px; |
||||
|
} |
||||
|
|
||||
|
&-action-buttons { |
||||
|
visibility: hidden; |
||||
|
.mat-icon { |
||||
|
color: rgba(0, 0, 0, 0.38); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.show-buttons { |
||||
|
visibility: visible; |
||||
|
} |
||||
|
|
||||
|
.green-button { |
||||
|
color: #00695C; |
||||
|
} |
||||
|
|
||||
|
.red-button { |
||||
|
color: #D12730; |
||||
|
} |
||||
|
|
||||
|
.mat-form-field { |
||||
|
font-size: 16px; |
||||
|
letter-spacing: 0.15px; |
||||
|
color: rgba(0, 0, 0, 0.76); |
||||
|
} |
||||
|
|
||||
|
textarea { |
||||
|
letter-spacing: 0.15px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,295 @@ |
|||||
|
///
|
||||
|
/// 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, Input, OnInit } from '@angular/core'; |
||||
|
import { select, Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { AlarmCommentService } from '@core/http/alarm-comment.service'; |
||||
|
import { AbstractControl, FormBuilder, FormGroup } from '@angular/forms'; |
||||
|
import { DialogService } from '@core/services/dialog.service'; |
||||
|
import { AuthUser, User } from '@shared/models/user.model'; |
||||
|
import { getCurrentAuthUser, selectUserDetails } from '@core/auth/auth.selectors'; |
||||
|
import { Direction, SortOrder } from '@shared/models/page/sort-order'; |
||||
|
import { MAX_SAFE_PAGE_SIZE, PageLink } from '@shared/models/page/page-link'; |
||||
|
import { DateAgoPipe } from '@shared/pipe/date-ago.pipe'; |
||||
|
import { map } from 'rxjs/operators'; |
||||
|
import { AlarmComment, AlarmCommentInfo, AlarmCommentType } from '@shared/models/alarm.models'; |
||||
|
import { UtilsService } from '@core/services/utils.service'; |
||||
|
import { EntityType } from '@shared/models/entity-type.models'; |
||||
|
|
||||
|
interface AlarmCommentsDisplayData { |
||||
|
commentId?: string, |
||||
|
displayName?: string, |
||||
|
createdDateAgo?: string, |
||||
|
edit?: boolean, |
||||
|
isEdited?: boolean, |
||||
|
editedDateAgo?: string, |
||||
|
showActions?: boolean, |
||||
|
commentText?: string, |
||||
|
isSystemComment?: boolean, |
||||
|
avatarBgColor?: string |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-alarm-comment', |
||||
|
templateUrl: './alarm-comment.component.html', |
||||
|
styleUrls: ['./alarm-comment.component.scss'] |
||||
|
}) |
||||
|
export class AlarmCommentComponent implements OnInit { |
||||
|
@Input() |
||||
|
alarmId: string; |
||||
|
|
||||
|
@Input() |
||||
|
commentsHeaderEnabled: boolean = true; |
||||
|
|
||||
|
authUser: AuthUser; |
||||
|
|
||||
|
alarmCommentFormGroup: FormGroup; |
||||
|
|
||||
|
alarmComments: Array<AlarmComment>; |
||||
|
|
||||
|
displayData: Array<AlarmCommentsDisplayData> = new Array<AlarmCommentsDisplayData>(); |
||||
|
|
||||
|
alarmCommentSortOrder: SortOrder = { |
||||
|
property: 'createdTime', |
||||
|
direction: Direction.DESC |
||||
|
}; |
||||
|
|
||||
|
editMode: boolean = false; |
||||
|
|
||||
|
userDisplayName$ = this.store.pipe( |
||||
|
select(selectUserDetails), |
||||
|
map((user) => this.getUserDisplayName(user)) |
||||
|
); |
||||
|
|
||||
|
currentUserDisplayName: string; |
||||
|
currentUserAvatarColor: string; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
private translate: TranslateService, |
||||
|
private alarmCommentService: AlarmCommentService, |
||||
|
public fb: FormBuilder, |
||||
|
private dialogService: DialogService, |
||||
|
public dateAgoPipe: DateAgoPipe, |
||||
|
private utilsService: UtilsService) { |
||||
|
|
||||
|
this.authUser = getCurrentAuthUser(store); |
||||
|
|
||||
|
this.alarmCommentFormGroup = this.fb.group( |
||||
|
{ |
||||
|
alarmCommentEdit: [''], |
||||
|
alarmComment: [''] |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.loadAlarmComments(); |
||||
|
this.currentUserAvatarColor = this.utilsService.stringToHslColor(this.currentUserDisplayName, |
||||
|
60, 40); |
||||
|
} |
||||
|
|
||||
|
loadAlarmComments(): void { |
||||
|
this.alarmCommentService.getAlarmComments(this.alarmId, new PageLink(MAX_SAFE_PAGE_SIZE, 0, null, |
||||
|
this.alarmCommentSortOrder)).subscribe( |
||||
|
(pagedData) => { |
||||
|
this.alarmComments = pagedData.data; |
||||
|
this.displayData.length = 0; |
||||
|
for (let alarmComment of pagedData.data) { |
||||
|
let displayDataElement: AlarmCommentsDisplayData = {}; |
||||
|
displayDataElement.createdDateAgo = this.dateAgoPipe.transform(alarmComment.createdTime); |
||||
|
displayDataElement.commentText = alarmComment.comment.text; |
||||
|
displayDataElement.isSystemComment = alarmComment.type === AlarmCommentType.SYSTEM; |
||||
|
if (alarmComment.type === AlarmCommentType.OTHER) { |
||||
|
displayDataElement.commentId = alarmComment.id.id; |
||||
|
displayDataElement.displayName = this.getUserDisplayName(alarmComment); |
||||
|
displayDataElement.edit = false; |
||||
|
displayDataElement.isEdited = alarmComment.comment.edited; |
||||
|
displayDataElement.editedDateAgo = this.dateAgoPipe.transform(alarmComment.comment.editedOn).toLowerCase(); |
||||
|
displayDataElement.showActions = false; |
||||
|
displayDataElement.isSystemComment = false; |
||||
|
displayDataElement.avatarBgColor = this.utilsService.stringToHslColor(displayDataElement.displayName, |
||||
|
40, 60); |
||||
|
} |
||||
|
this.displayData.push(displayDataElement); |
||||
|
} |
||||
|
} |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
changeSortDirection() { |
||||
|
let currentDirection = this.alarmCommentSortOrder.direction; |
||||
|
this.alarmCommentSortOrder.direction = currentDirection === Direction.DESC ? Direction.ASC : Direction.DESC; |
||||
|
this.loadAlarmComments(); |
||||
|
} |
||||
|
|
||||
|
saveComment(): void { |
||||
|
const commentInputValue: string = this.getAlarmCommentFormControl().value; |
||||
|
if (commentInputValue) { |
||||
|
const comment: AlarmComment = { |
||||
|
alarmId: { |
||||
|
id: this.alarmId, |
||||
|
entityType: EntityType.ALARM |
||||
|
}, |
||||
|
type: AlarmCommentType.OTHER, |
||||
|
comment: { |
||||
|
text: commentInputValue |
||||
|
} |
||||
|
} |
||||
|
this.doSave(comment); |
||||
|
this.clearCommentInput(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
saveEditedComment(commentId: string): void { |
||||
|
const commentEditInputValue: string = this.getAlarmCommentEditFormControl().value; |
||||
|
if (commentEditInputValue) { |
||||
|
const editedComment: AlarmComment = this.getAlarmCommentById(commentId); |
||||
|
editedComment.comment.text = commentEditInputValue; |
||||
|
this.doSave(editedComment); |
||||
|
this.clearCommentEditInput(); |
||||
|
this.editMode = false; |
||||
|
this.getAlarmCommentFormControl().enable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private doSave(comment: AlarmComment): void { |
||||
|
this.alarmCommentService.saveAlarmComment(this.alarmId, comment).subscribe( |
||||
|
() => { |
||||
|
this.loadAlarmComments(); |
||||
|
} |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
editComment(commentId: string): void { |
||||
|
const commentDisplayData = this.getDataElementByCommentId(commentId); |
||||
|
commentDisplayData.edit = true; |
||||
|
this.editMode = true; |
||||
|
this.getAlarmCommentEditFormControl().patchValue(commentDisplayData.commentText); |
||||
|
this.getAlarmCommentFormControl().disable({emitEvent: false}); |
||||
|
} |
||||
|
|
||||
|
cancelEdit(commentId: string): void { |
||||
|
const commentDisplayData = this.getDataElementByCommentId(commentId); |
||||
|
commentDisplayData.edit = false; |
||||
|
this.editMode = false; |
||||
|
this.getAlarmCommentFormControl().enable({emitEvent: false}); |
||||
|
} |
||||
|
|
||||
|
deleteComment(commentId: string): void { |
||||
|
const alarmCommentInfo: AlarmComment = this.getAlarmCommentById(commentId); |
||||
|
const commentText: string = alarmCommentInfo.comment.text; |
||||
|
this.dialogService.confirm( |
||||
|
this.translate.instant('alarm-comment.delete-alarm-comment'), |
||||
|
commentText, |
||||
|
this.translate.instant('action.cancel'), |
||||
|
this.translate.instant('action.delete')).subscribe( |
||||
|
(result) => { |
||||
|
if (result) { |
||||
|
this.alarmCommentService.deleteAlarmComments(this.alarmId, commentId).subscribe( |
||||
|
() => { |
||||
|
this.loadAlarmComments(); |
||||
|
} |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
getSortDirectionIcon() { |
||||
|
return this.alarmCommentSortOrder.direction === Direction.DESC ? 'arrow_downward' : 'arrow_upward' |
||||
|
} |
||||
|
|
||||
|
isDirectionAscending() { |
||||
|
return this.alarmCommentSortOrder.direction === Direction.ASC; |
||||
|
} |
||||
|
|
||||
|
isDirectionDescending() { |
||||
|
return this.alarmCommentSortOrder.direction === Direction.DESC; |
||||
|
} |
||||
|
|
||||
|
onCommentMouseEnter(commentId: string, displayDataIndex: number): void { |
||||
|
if (!this.editMode) { |
||||
|
const alarmUserId = this.getAlarmCommentById(commentId).userId.id; |
||||
|
if (this.authUser.userId === alarmUserId) { |
||||
|
this.displayData[displayDataIndex].showActions = true; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
onCommentMouseLeave(displayDataIndex: number): void { |
||||
|
this.displayData[displayDataIndex].showActions = false; |
||||
|
} |
||||
|
|
||||
|
getUserInitials(userName: string): string { |
||||
|
let initials = ''; |
||||
|
const userNameSplit = userName.split(' '); |
||||
|
initials += userNameSplit[0].charAt(0).toUpperCase(); |
||||
|
if (userNameSplit.length > 1) { |
||||
|
initials += userNameSplit[userNameSplit.length - 1].charAt(0).toUpperCase(); |
||||
|
} |
||||
|
return initials; |
||||
|
} |
||||
|
|
||||
|
getCurrentUserBgColor(userDisplayName: string) { |
||||
|
return this.utilsService.stringToHslColor(userDisplayName, 40, 60); |
||||
|
} |
||||
|
|
||||
|
private getUserDisplayName(alarmCommentInfo: AlarmCommentInfo | User): string { |
||||
|
let name = ''; |
||||
|
if ((alarmCommentInfo.firstName && alarmCommentInfo.firstName.length > 0) || |
||||
|
(alarmCommentInfo.lastName && alarmCommentInfo.lastName.length > 0)) { |
||||
|
if (alarmCommentInfo.firstName) { |
||||
|
name += alarmCommentInfo.firstName; |
||||
|
} |
||||
|
if (alarmCommentInfo.lastName) { |
||||
|
if (name.length > 0) { |
||||
|
name += ' '; |
||||
|
} |
||||
|
name += alarmCommentInfo.lastName; |
||||
|
} |
||||
|
} else { |
||||
|
name = alarmCommentInfo.email; |
||||
|
} |
||||
|
return name; |
||||
|
} |
||||
|
|
||||
|
getAlarmCommentFormControl(): AbstractControl { |
||||
|
return this.alarmCommentFormGroup.get('alarmComment'); |
||||
|
} |
||||
|
|
||||
|
getAlarmCommentEditFormControl(): AbstractControl { |
||||
|
return this.alarmCommentFormGroup.get('alarmCommentEdit'); |
||||
|
} |
||||
|
|
||||
|
private clearCommentInput(): void { |
||||
|
this.getAlarmCommentFormControl().patchValue(''); |
||||
|
} |
||||
|
|
||||
|
private clearCommentEditInput(): void { |
||||
|
this.getAlarmCommentEditFormControl().patchValue(''); |
||||
|
} |
||||
|
|
||||
|
private getAlarmCommentById(id: string): AlarmComment { |
||||
|
return this.alarmComments.find(comment => comment.id.id === id); |
||||
|
} |
||||
|
|
||||
|
private getDataElementByCommentId(commentId: string): AlarmCommentsDisplayData { |
||||
|
return this.displayData.find(commentDisplayData => commentDisplayData.commentId === commentId); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
///
|
||||
|
/// 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 { HasUUID } from '@shared/models/id/has-uuid'; |
||||
|
|
||||
|
export class AlarmCommentId implements HasUUID { |
||||
|
id: string; |
||||
|
constructor(id: string) { |
||||
|
this.id = id; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,58 @@ |
|||||
|
///
|
||||
|
/// 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 { Inject, Pipe, PipeTransform } from '@angular/core'; |
||||
|
import { DAY, HOUR, MINUTE, SECOND, WEEK, YEAR } from '@shared/models/time/time.models'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
|
||||
|
const intervals = { |
||||
|
years: YEAR, |
||||
|
months: DAY * 30, |
||||
|
weeks: WEEK, |
||||
|
days: DAY, |
||||
|
hr: HOUR, |
||||
|
min: MINUTE, |
||||
|
sec: SECOND |
||||
|
}; |
||||
|
|
||||
|
@Pipe({ |
||||
|
name: 'dateAgo' |
||||
|
}) |
||||
|
export class DateAgoPipe implements PipeTransform { |
||||
|
|
||||
|
constructor(@Inject(TranslateService) private translate: TranslateService) { |
||||
|
|
||||
|
} |
||||
|
|
||||
|
transform(value: number): string { |
||||
|
if (value) { |
||||
|
const ms = Math.floor((+new Date() - +new Date(value))); |
||||
|
if (ms < 29 * SECOND) { // less than 30 seconds ago will show as 'Just now'
|
||||
|
return this.translate.instant('timewindow.just-now'); |
||||
|
} |
||||
|
let counter; |
||||
|
// tslint:disable-next-line:forin
|
||||
|
for (const i in intervals) { |
||||
|
counter = Math.floor(ms / intervals[i]); |
||||
|
if (counter > 0) { |
||||
|
return this.translate.instant(`timewindow.${i}`, {[i]: counter}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return ''; |
||||
|
} |
||||
|
|
||||
|
} |
||||
Loading…
Reference in new issue