28 changed files with 1163 additions and 57 deletions
@ -0,0 +1,27 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2022 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 fxFlex="100" fxLayout="row" fxLayoutAlign="start center"> |
|||
<mat-button-toggle-group class="tb-notification-unread-toggle-group" |
|||
style="width: 180px;" |
|||
[ngModel]="entitiesTableConfig.componentsData.unreadOnly" |
|||
(ngModelChange)="changeUnreadOnly($event)"> |
|||
<mat-button-toggle fxFlex [value]=true>Unread</mat-button-toggle> |
|||
<mat-button-toggle fxFlex [value]=false>{{ 'notification.all' | translate }}</mat-button-toggle> |
|||
</mat-button-toggle-group> |
|||
|
|||
</div> |
|||
@ -0,0 +1,71 @@ |
|||
/** |
|||
* Copyright © 2016-2022 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 "../../../../../../theme"; |
|||
|
|||
:host ::ng-deep { |
|||
.mat-button-toggle-group.tb-notification-unread-toggle-group { |
|||
&.mat-button-toggle-group-appearance-standard { |
|||
border: none; |
|||
border-radius: 14px; |
|||
|
|||
.mat-button-toggle + .mat-button-toggle { |
|||
border-left: none; |
|||
} |
|||
} |
|||
|
|||
.mat-button-toggle { |
|||
background: rgba(0, 0, 0, 0.06); |
|||
height: 28px; |
|||
align-items: center; |
|||
display: flex; |
|||
|
|||
.mat-button-toggle-ripple { |
|||
top: 2px; |
|||
left: 2px; |
|||
right: 2px; |
|||
bottom: 2px; |
|||
border-radius: 12px; |
|||
} |
|||
} |
|||
|
|||
.mat-button-toggle-button { |
|||
color: #959595; |
|||
} |
|||
|
|||
.mat-button-toggle-focus-overlay { |
|||
border-radius: 14px; |
|||
margin: 2px; |
|||
} |
|||
|
|||
.mat-button-toggle-checked .mat-button-toggle-button { |
|||
background-color: $tb-primary-color; |
|||
color: #fff; |
|||
border-radius: 14px; |
|||
margin-left: 2px; |
|||
margin-right: 2px; |
|||
} |
|||
|
|||
.mat-button-toggle-appearance-standard .mat-button-toggle-label-content { |
|||
line-height: 24px; |
|||
font-size: 16px; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.mat-button-toggle-checked.mat-button-toggle-appearance-standard:not(.mat-button-toggle-disabled):hover .mat-button-toggle-focus-overlay { |
|||
opacity: .01; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
///
|
|||
/// Copyright © 2016-2022 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 } from '@angular/core'; |
|||
import { EntityTableHeaderComponent } from '@home/components/entity/entity-table-header.component'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { Notification } from '@shared/models/notification.models'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-inbox-table-header', |
|||
templateUrl: './inbox-table-header.component.html', |
|||
styleUrls: ['inbox-table-header.component.scss'] |
|||
}) |
|||
export class InboxTableHeaderComponent extends EntityTableHeaderComponent<Notification> { |
|||
|
|||
constructor(protected store: Store<AppState>) { |
|||
super(store); |
|||
} |
|||
|
|||
changeUnreadOnly(unreadOnly: boolean) { |
|||
this.entitiesTableConfig.componentsData.unreadOnly = unreadOnly; |
|||
this.entitiesTableConfig.getTable().resetSortAndFilter(true); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,143 @@ |
|||
///
|
|||
/// Copyright © 2016-2022 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 { |
|||
CellActionDescriptor, |
|||
DateEntityTableColumn, |
|||
EntityTableColumn, |
|||
EntityTableConfig |
|||
} from '@home/models/entity/entities-table-config.models'; |
|||
import { |
|||
NotificationRequest, |
|||
NotificationRequestStatus, |
|||
NotificationRequestStatusTranslateMap, |
|||
NotificationTemplate |
|||
} from '@shared/models/notification.models'; |
|||
import { NotificationService } from '@core/http/notification.service'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
import { EntityTypeResource } from '@shared/models/entity-type.models'; |
|||
import { Direction } from '@shared/models/page/sort-order'; |
|||
import { DatePipe } from '@angular/common'; |
|||
import { EntityAction } from '@home/models/entity/entity-component.models'; |
|||
import { |
|||
RequestNotificationDialogComponent, |
|||
RequestNotificationDialogData |
|||
} from '@home/pages/notification-center/request-table/request-notification-dialog.componet'; |
|||
|
|||
export class RequestTableConfig extends EntityTableConfig<NotificationRequest> { |
|||
|
|||
constructor(private notificationService: NotificationService, |
|||
private translate: TranslateService, |
|||
private dialog: MatDialog, |
|||
private datePipe: DatePipe) { |
|||
super(); |
|||
this.loadDataOnInit = false; |
|||
this.searchEnabled = false; |
|||
this.entityTranslations = { |
|||
noEntities: 'notification.no-notification-request', |
|||
}; |
|||
this.entityResources = {} as EntityTypeResource<NotificationRequest>; |
|||
|
|||
this.entitiesFetchFunction = pageLink => this.notificationService.getNotificationRequests(pageLink); |
|||
|
|||
this.deleteEnabled = (request) => request.status === NotificationRequestStatus.SCHEDULED; |
|||
this.deleteEntityTitle = () => this.translate.instant('notification.delete-request-title'); |
|||
this.deleteEntityContent = () => this.translate.instant('notification.delete-request-text'); |
|||
this.deleteEntity = id => this.notificationService.deleteNotificationRequest(id.id); |
|||
|
|||
this.cellActionDescriptors = this.configureCellActions(); |
|||
|
|||
this.onEntityAction = action => this.onRequestAction(action); |
|||
|
|||
this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; |
|||
|
|||
this.columns.push( |
|||
new DateEntityTableColumn<NotificationRequest>('createdTime', 'notification.created-time', this.datePipe, '150px'), |
|||
new EntityTableColumn<NotificationRequest>('status', 'notification.status', '100%', |
|||
request => this.requestStatus(request.status), request => this.requestStatusStyle(request.status)), |
|||
// new EntityTableColumn<NotificationRequest>('deliveryMethods', 'notification.delivery-method', '20%',
|
|||
// (request) => request.deliveryMethods.toString(),
|
|||
// () => ({}), false)
|
|||
); |
|||
} |
|||
|
|||
private configureCellActions(): Array<CellActionDescriptor<NotificationRequest>> { |
|||
return []; |
|||
} |
|||
|
|||
private createRequest($event: Event, request: NotificationRequest, isAdd = false) { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
this.dialog.open<RequestNotificationDialogComponent, RequestNotificationDialogData, |
|||
NotificationTemplate>(RequestNotificationDialogComponent, { |
|||
disableClose: true, |
|||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
|||
data: { |
|||
isAdd, |
|||
request |
|||
} |
|||
}).afterClosed() |
|||
.subscribe((res) => { |
|||
if (res) { |
|||
this.updateData(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
private onRequestAction(action: EntityAction<NotificationRequest>): boolean { |
|||
switch (action.action) { |
|||
case 'add': |
|||
this.createRequest(action.event, action.entity, true); |
|||
return true; |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
private requestStatus(status: NotificationRequestStatus): string { |
|||
const translateKey = NotificationRequestStatusTranslateMap.get(status); |
|||
let backgroundColor = 'rgba(25, 128, 56, 0.08)'; |
|||
switch (status) { |
|||
case NotificationRequestStatus.SCHEDULED: |
|||
backgroundColor = 'rgba(48, 86, 128, 0.08)'; |
|||
break; |
|||
case NotificationRequestStatus.PROCESSING: |
|||
backgroundColor = 'rgba(212, 125, 24, 0.08)'; |
|||
break; |
|||
} |
|||
return `<div style="border-radius: 16px; height: 32px; line-height: 32px; padding: 0 12px; width: fit-content; background-color: ${backgroundColor}">
|
|||
${this.translate.instant(translateKey)} |
|||
</div>`;
|
|||
} |
|||
|
|||
private requestStatusStyle(status: NotificationRequestStatus): object { |
|||
const styleObj = { |
|||
fontSize: '14px', |
|||
color: '#198038', |
|||
cursor: 'pointer' |
|||
}; |
|||
switch (status) { |
|||
case NotificationRequestStatus.SCHEDULED: |
|||
styleObj.color = '#305680'; |
|||
break; |
|||
case NotificationRequestStatus.PROCESSING: |
|||
styleObj.color = '#D47D18'; |
|||
break; |
|||
} |
|||
return styleObj; |
|||
} |
|||
} |
|||
@ -0,0 +1,78 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2022 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. |
|||
|
|||
--> |
|||
<mat-toolbar color="primary"> |
|||
<h2 translate>notification.new-notification</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> |
|||
<mat-horizontal-stepper [linear]="true" labelPosition="end" #createNotification |
|||
[orientation]="(stepperOrientation | async)" |
|||
(selectionChange)="changeStep($event)"> |
|||
<ng-template matStepperIcon="edit"> |
|||
<mat-icon>check</mat-icon> |
|||
</ng-template> |
|||
<mat-step [stepControl]="notificationRequestForm"> |
|||
<ng-template matStepLabel>{{ 'notification.compose' | translate }}</ng-template> |
|||
<form [formGroup]="notificationRequestForm" style="padding-bottom: 16px;"> |
|||
<tb-template-autocomplete |
|||
required |
|||
formControlName="templateId"> |
|||
</tb-template-autocomplete> |
|||
<tb-entity-list |
|||
required |
|||
formControlName="targets" |
|||
labelText="notification.targets" |
|||
placeholderText="notification.target" |
|||
requiredText="notification.targets-required" |
|||
[entityType]="entityType.NOTIFICATION_TARGET"> |
|||
</tb-entity-list> |
|||
</form> |
|||
</mat-step> |
|||
<mat-step> |
|||
<ng-template matStepLabel>{{ 'notification.review' | translate }}</ng-template> |
|||
<mat-progress-spinner color="warn" mode="indeterminate" |
|||
strokeWidth="5" *ngIf="(isLoading$ | async) && !preview"> |
|||
</mat-progress-spinner> |
|||
<div *ngIf="preview"> |
|||
<section class="preview-group"> |
|||
<div fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center"> |
|||
<mat-icon>supervisor_account</mat-icon> |
|||
<div class="total-recipients">{{ 'notification.recipients-count' | translate : {count: preview.totalRecipientsCount} }}</div> |
|||
</div> |
|||
</section> |
|||
</div> |
|||
</mat-step> |
|||
</mat-horizontal-stepper> |
|||
</div> |
|||
<mat-divider></mat-divider> |
|||
<div mat-dialog-actions fxLayout="row"> |
|||
<button mat-stroked-button *ngIf="selectedIndex > 0" |
|||
(click)="backStep()">{{ 'action.back' | translate }}</button> |
|||
<span fxFlex></span> |
|||
<button mat-raised-button |
|||
color="primary" |
|||
(click)="nextStep()">{{ nextStepLabel() | translate }}</button> |
|||
</div> |
|||
@ -0,0 +1,81 @@ |
|||
/** |
|||
* Copyright © 2016-2022 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 "../../../../../../scss/constants"; |
|||
|
|||
:host-context(.tb-fullscreen-dialog .mat-dialog-container) { |
|||
width: 640px; |
|||
|
|||
.preview-group { |
|||
padding: 16px; |
|||
margin-bottom: 10px; |
|||
border: 1px groove rgba(0, 0, 0, .12); |
|||
border-radius: 4px; |
|||
|
|||
.mat-icon { |
|||
color: rgba(0, 0, 0, 0.38); |
|||
} |
|||
|
|||
.total-recipients { |
|||
font-weight: 500; |
|||
letter-spacing: .25px; |
|||
} |
|||
} |
|||
|
|||
@media #{$mat-gt-xs} { |
|||
.mat-dialog-content { |
|||
max-height: 75vh; |
|||
} |
|||
} |
|||
} |
|||
|
|||
:host ::ng-deep { |
|||
.mat-dialog-content { |
|||
display: flex; |
|||
flex-direction: column; |
|||
height: 100%; |
|||
padding: 0 !important; |
|||
|
|||
.mat-stepper-horizontal { |
|||
display: flex; |
|||
flex-direction: column; |
|||
height: 100%; |
|||
overflow: hidden; |
|||
@media #{$mat-lt-sm} { |
|||
.mat-step-label { |
|||
white-space: normal; |
|||
overflow: visible; |
|||
.mat-step-text-label { |
|||
overflow: visible; |
|||
} |
|||
} |
|||
} |
|||
.mat-horizontal-content-container { |
|||
height: 400px; |
|||
max-height: 100%; |
|||
width: 100%;; |
|||
overflow-y: auto; |
|||
scrollbar-gutter: stable; |
|||
@media #{$mat-gt-sm} { |
|||
min-width: 500px; |
|||
} |
|||
} |
|||
.mat-horizontal-stepper-content[aria-expanded=true] { |
|||
height: 100%; |
|||
position: relative; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,155 @@ |
|||
///
|
|||
/// Copyright © 2016-2022 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 { NotificationRequest, NotificationRequestPreview } from '@shared/models/notification.models'; |
|||
import { Component, Inject, OnDestroy, ViewChild } from '@angular/core'; |
|||
import { DialogComponent } from '@shared/components/dialog.component'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { Router } from '@angular/router'; |
|||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; |
|||
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; |
|||
import { NotificationService } from '@core/http/notification.service'; |
|||
import { deepTrim, isDefined } from '@core/utils'; |
|||
import { Observable, Subject } from 'rxjs'; |
|||
import { EntityType } from '@shared/models/entity-type.models'; |
|||
import { BreakpointObserver } from '@angular/cdk/layout'; |
|||
import { MatStepper } from '@angular/material/stepper'; |
|||
import { StepperOrientation, StepperSelectionEvent } from '@angular/cdk/stepper'; |
|||
import { MediaBreakpoints } from '@shared/models/constants'; |
|||
import { map } from 'rxjs/operators'; |
|||
|
|||
export interface RequestNotificationDialogData { |
|||
request?: NotificationRequest; |
|||
isAdd?: boolean; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-request-notification-dialog', |
|||
templateUrl: './request-notification-dialog.component.html', |
|||
styleUrls: ['./request-notification-dialog.component.scss'] |
|||
}) |
|||
export class RequestNotificationDialogComponent extends |
|||
DialogComponent<RequestNotificationDialogComponent, NotificationRequest> implements OnDestroy { |
|||
|
|||
@ViewChild('createNotification', {static: true}) createNotification: MatStepper; |
|||
stepperOrientation: Observable<StepperOrientation>; |
|||
isAdd = true; |
|||
entityType = EntityType; |
|||
notificationRequestForm: FormGroup; |
|||
|
|||
selectedIndex = 0; |
|||
preview: NotificationRequestPreview = null; |
|||
|
|||
private readonly destroy$ = new Subject<void>(); |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected router: Router, |
|||
protected dialogRef: MatDialogRef<RequestNotificationDialogComponent, NotificationRequest>, |
|||
@Inject(MAT_DIALOG_DATA) public data: RequestNotificationDialogData, |
|||
private breakpointObserver: BreakpointObserver, |
|||
private fb: FormBuilder, |
|||
private notificationService: NotificationService) { |
|||
super(store, router, dialogRef); |
|||
|
|||
if (isDefined(data.isAdd)) { |
|||
this.isAdd = data.isAdd; |
|||
} |
|||
|
|||
this.stepperOrientation = this.breakpointObserver.observe(MediaBreakpoints['gt-xs']) |
|||
.pipe(map(({matches}) => matches ? 'horizontal' : 'vertical')); |
|||
|
|||
this.notificationRequestForm = this.fb.group({ |
|||
templateId: [null, Validators.required], |
|||
targets: [null, Validators.required] |
|||
}); |
|||
} |
|||
|
|||
ngOnDestroy() { |
|||
super.ngOnDestroy(); |
|||
this.destroy$.next(); |
|||
this.destroy$.complete(); |
|||
} |
|||
|
|||
cancel(): void { |
|||
this.dialogRef.close(null); |
|||
} |
|||
|
|||
nextStepLabel(): string { |
|||
if (this.selectedIndex >= this.maxStepperIndex) { |
|||
return 'action.send'; |
|||
} |
|||
return 'action.next'; |
|||
} |
|||
|
|||
changeStep($event: StepperSelectionEvent) { |
|||
this.selectedIndex = $event.selectedIndex; |
|||
if (this.selectedIndex === this.maxStepperIndex) { |
|||
this.getPreview(); |
|||
} |
|||
} |
|||
|
|||
backStep() { |
|||
this.createNotification.previous(); |
|||
} |
|||
|
|||
nextStep() { |
|||
if (this.selectedIndex >= this.maxStepperIndex) { |
|||
this.add(); |
|||
} else { |
|||
this.createNotification.next(); |
|||
} |
|||
} |
|||
|
|||
private get maxStepperIndex(): number { |
|||
return this.createNotification?._steps?.length - 1; |
|||
} |
|||
|
|||
private add(): void { |
|||
if (this.allValid()) { |
|||
this.notificationService.createNotificationRequest(this.notificationFormValue).subscribe( |
|||
(notification) => this.dialogRef.close(notification) |
|||
); |
|||
} |
|||
} |
|||
|
|||
private getPreview() { |
|||
if (this.allValid()) { |
|||
this.preview = null; |
|||
this.notificationService.getNotificationRequestPreview(this.notificationFormValue).subscribe( |
|||
(preview) => this.preview = preview |
|||
); |
|||
} |
|||
} |
|||
|
|||
private get notificationFormValue(): NotificationRequest { |
|||
const formValue: NotificationRequest = deepTrim(this.notificationRequestForm.value); |
|||
formValue.additionalConfig = {sendingDelayInSec: 0}; |
|||
return formValue; |
|||
} |
|||
|
|||
private allValid(): boolean { |
|||
return !this.createNotification.steps.find((item, index) => { |
|||
if (item.stepControl?.invalid) { |
|||
item.interacted = true; |
|||
this.createNotification.selectedIndex = index; |
|||
return true; |
|||
} else { |
|||
return false; |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,57 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2022 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. |
|||
|
|||
--> |
|||
<mat-form-field [formGroup]="selectTemplateFormGroup" class="mat-block"> |
|||
<input matInput type="text" placeholder="{{ 'notification.template' | translate }}" |
|||
#templateInput |
|||
formControlName="templateName" |
|||
(focusin)="onFocus()" |
|||
[required]="required" |
|||
[matAutocomplete]="templateAutocomplete"> |
|||
<button *ngIf="selectTemplateFormGroup.get('templateName').value && !disabled" |
|||
type="button" |
|||
matSuffix mat-button mat-icon-button aria-label="Clear" |
|||
(click)="clear()"> |
|||
<mat-icon class="material-icons">close</mat-icon> |
|||
</button> |
|||
<mat-autocomplete class="tb-autocomplete" |
|||
#templateAutocomplete="matAutocomplete" |
|||
[displayWith]="displayTemplateFn" |
|||
> |
|||
<mat-option *ngFor="let template of filteredTemplate | async" [value]="template" class="template-option"> |
|||
<span class="template-option-name" [innerHTML]="template.name | highlight:searchText"></span> |
|||
<mat-chip-list> |
|||
<mat-chip disabled *ngFor="let method of template.configuration.deliveryMethodsTemplates | keyvalue"> |
|||
{{ notificationDeliveryMethodTranslateMap.get(method.key) | translate }} |
|||
</mat-chip> |
|||
</mat-chip-list> |
|||
</mat-option> |
|||
<mat-option *ngIf="!(filteredTemplate | async)?.length" [value]="null" class="tb-not-found"> |
|||
<div class="tb-not-found-content" (click)="$event.stopPropagation()"> |
|||
<div *ngIf="!textIsNotEmpty(searchText); else searchNotEmpty"> |
|||
<span translate>notification.no-notification-templates</span> |
|||
</div> |
|||
<ng-template #searchNotEmpty> |
|||
<span>Not template matching {{ truncate.transform(searchText, true, 6, '...') }}</span> |
|||
</ng-template> |
|||
</div> |
|||
</mat-option> |
|||
</mat-autocomplete> |
|||
<mat-error *ngIf="selectTemplateFormGroup.get('templateName').hasError('required')"> |
|||
{{ 'notification.template-required' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
@ -0,0 +1,42 @@ |
|||
/** |
|||
* Copyright © 2016-2022 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. |
|||
*/ |
|||
|
|||
::ng-deep { |
|||
.template-option { |
|||
.mat-option-text { |
|||
display: flex; |
|||
flex-direction: row; |
|||
align-items: center; |
|||
margin-bottom: 4px; |
|||
} |
|||
|
|||
.template-option-name { |
|||
margin-right: 8px; |
|||
} |
|||
|
|||
.mat-chip { |
|||
&.mat-standard-chip { |
|||
padding: 4px 8px; |
|||
border-radius: 14px; |
|||
min-height: 28px; |
|||
|
|||
&.mat-chip-disabled { |
|||
opacity: 1; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,213 @@ |
|||
///
|
|||
/// Copyright © 2016-2022 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, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; |
|||
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; |
|||
import { Observable, of } from 'rxjs'; |
|||
import { catchError, debounceTime, distinctUntilChanged, map, share, switchMap, tap } from 'rxjs/operators'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
|||
import { EntityId } from '@shared/models/id/entity-id'; |
|||
import { EntityService } from '@core/http/entity.service'; |
|||
import { TruncatePipe } from '@shared/pipe/truncate.pipe'; |
|||
import { PageLink } from '@shared/models/page/page-link'; |
|||
import { Direction } from '@shared/models/page/sort-order'; |
|||
import { emptyPageData } from '@shared/models/page/page-data'; |
|||
import { NotificationDeliveryMethodTranslateMap, NotificationTemplate } from '@shared/models/notification.models'; |
|||
import { NotificationService } from '@core/http/notification.service'; |
|||
import { isEqual } from '@core/utils'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-template-autocomplete', |
|||
templateUrl: './template-autocomplete.component.html', |
|||
styleUrls: ['./template-autocomplete.component.scss'], |
|||
providers: [{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => TemplateAutocompleteComponent), |
|||
multi: true |
|||
}] |
|||
}) |
|||
export class TemplateAutocompleteComponent implements ControlValueAccessor, OnInit { |
|||
|
|||
notificationDeliveryMethodTranslateMap = NotificationDeliveryMethodTranslateMap; |
|||
selectTemplateFormGroup: FormGroup; |
|||
|
|||
@Input() |
|||
labelText: string; |
|||
|
|||
@Input() |
|||
requiredText: string; |
|||
|
|||
private requiredValue: boolean; |
|||
get required(): boolean { |
|||
return this.requiredValue; |
|||
} |
|||
@Input() |
|||
set required(value: boolean) { |
|||
this.requiredValue = coerceBooleanProperty(value); |
|||
} |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
@ViewChild('templateInput', {static: true}) templateInput: ElementRef; |
|||
|
|||
filteredTemplate: Observable<Array<NotificationTemplate>>; |
|||
|
|||
searchText = ''; |
|||
|
|||
private modelValue: EntityId | null; |
|||
private dirty = false; |
|||
|
|||
private propagateChange = (v: any) => { }; |
|||
|
|||
constructor(private store: Store<AppState>, |
|||
public translate: TranslateService, |
|||
public truncate: TruncatePipe, |
|||
private entityService: EntityService, |
|||
private notificationService: NotificationService, |
|||
private fb: FormBuilder) { |
|||
this.selectTemplateFormGroup = this.fb.group({ |
|||
templateName: [null] |
|||
}); |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
ngOnInit() { |
|||
if (this.required) { |
|||
this.selectTemplateFormGroup.get('templateName').addValidators(Validators.required); |
|||
this.selectTemplateFormGroup.get('templateName').updateValueAndValidity({emitEvent: false}); |
|||
} |
|||
this.filteredTemplate = this.selectTemplateFormGroup.get('templateName').valueChanges |
|||
.pipe( |
|||
debounceTime(150), |
|||
tap(value => { |
|||
let modelValue; |
|||
if (typeof value === 'string' || !value) { |
|||
modelValue = null; |
|||
} else { |
|||
modelValue = value.id; |
|||
} |
|||
this.updateView(modelValue); |
|||
if (value === null) { |
|||
this.clear(); |
|||
} |
|||
}), |
|||
map(value => value ? (typeof value === 'string' ? value : value.name) : ''), |
|||
distinctUntilChanged(), |
|||
switchMap(name => this.fetchTemplate(name)), |
|||
share() |
|||
); |
|||
} |
|||
|
|||
ngAfterViewInit(): void {} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (this.disabled) { |
|||
this.selectTemplateFormGroup.disable({emitEvent: false}); |
|||
} else { |
|||
this.selectTemplateFormGroup.enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
textIsNotEmpty(text: string): boolean { |
|||
return (text && text.length > 0); |
|||
} |
|||
|
|||
writeValue(value: EntityId | null): void { |
|||
this.searchText = ''; |
|||
if (value != null) { |
|||
this.notificationService.getNotificationTemplateById(value.id, {ignoreLoading: true, ignoreErrors: true}).subscribe( |
|||
(entity) => { |
|||
this.modelValue = entity.id; |
|||
this.selectTemplateFormGroup.get('templateName').patchValue(entity, {emitEvent: false}); |
|||
}, |
|||
() => { |
|||
this.modelValue = null; |
|||
this.selectTemplateFormGroup.get('templateName').patchValue('', {emitEvent: false}); |
|||
if (value !== null) { |
|||
this.propagateChange(this.modelValue); |
|||
} |
|||
} |
|||
); |
|||
} else { |
|||
this.modelValue = null; |
|||
this.selectTemplateFormGroup.get('templateName').patchValue('', {emitEvent: false}); |
|||
} |
|||
this.dirty = true; |
|||
} |
|||
|
|||
onFocus() { |
|||
if (this.dirty) { |
|||
this.selectTemplateFormGroup.get('templateName').updateValueAndValidity({onlySelf: true, emitEvent: true}); |
|||
this.dirty = false; |
|||
} |
|||
} |
|||
|
|||
reset() { |
|||
this.selectTemplateFormGroup.get('templateName').patchValue('', {emitEvent: false}); |
|||
} |
|||
|
|||
updateView(value: EntityId | null) { |
|||
if (!isEqual(this.modelValue, value)) { |
|||
this.modelValue = value; |
|||
this.propagateChange(this.modelValue); |
|||
} |
|||
} |
|||
|
|||
displayTemplateFn(queue?: NotificationTemplate): string | undefined { |
|||
return queue ? queue.name : undefined; |
|||
} |
|||
|
|||
fetchTemplate(searchText?: string): Observable<Array<NotificationTemplate>> { |
|||
this.searchText = searchText; |
|||
const pageLink = new PageLink(10, 0, searchText, { |
|||
property: 'name', |
|||
direction: Direction.ASC |
|||
}); |
|||
return this.notificationService.getNotificationTemplates(pageLink, {ignoreLoading: true}).pipe( |
|||
catchError(() => of(emptyPageData<NotificationTemplate>())), |
|||
map(pageData => { |
|||
return pageData.data; |
|||
}) |
|||
); |
|||
} |
|||
|
|||
getDescription(value) { |
|||
return value.additionalInfo?.description ? value.additionalInfo.description : |
|||
this.translate.instant( |
|||
'queue.alt-description', |
|||
{submitStrategy: value.submitStrategy.type, processingStrategy: value.processingStrategy.type} |
|||
); |
|||
} |
|||
|
|||
clear() { |
|||
this.selectTemplateFormGroup.get('templateName').patchValue('', {emitEvent: true}); |
|||
setTimeout(() => { |
|||
this.templateInput.nativeElement.blur(); |
|||
this.templateInput.nativeElement.focus(); |
|||
}, 0); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue