Browse Source

UI: Add notification request and inbox table

pull/7980/head
Vladyslav_Prykhodko 4 years ago
parent
commit
00d04aec3c
  1. 8
      ui-ngx/src/app/core/http/entity.service.ts
  2. 16
      ui-ngx/src/app/core/http/notification.service.ts
  3. 27
      ui-ngx/src/app/modules/home/pages/notification-center/inbox-table/inbox-table-header.component.html
  4. 71
      ui-ngx/src/app/modules/home/pages/notification-center/inbox-table/inbox-table-header.component.scss
  5. 39
      ui-ngx/src/app/modules/home/pages/notification-center/inbox-table/inbox-table-header.component.ts
  6. 3
      ui-ngx/src/app/modules/home/pages/notification-center/notification-center.component.html
  7. 4
      ui-ngx/src/app/modules/home/pages/notification-center/notification-center.component.scss
  8. 5
      ui-ngx/src/app/modules/home/pages/notification-center/notification-center.component.ts
  9. 12
      ui-ngx/src/app/modules/home/pages/notification-center/notification-center.module.ts
  10. 81
      ui-ngx/src/app/modules/home/pages/notification-center/notification-table/inbox-table-config.ts
  11. 7
      ui-ngx/src/app/modules/home/pages/notification-center/notification-table/notification-table.component.ts
  12. 143
      ui-ngx/src/app/modules/home/pages/notification-center/notification-table/request-table-config.ts
  13. 78
      ui-ngx/src/app/modules/home/pages/notification-center/request-table/request-notification-dialog.component.html
  14. 81
      ui-ngx/src/app/modules/home/pages/notification-center/request-table/request-notification-dialog.component.scss
  15. 155
      ui-ngx/src/app/modules/home/pages/notification-center/request-table/request-notification-dialog.componet.ts
  16. 3
      ui-ngx/src/app/modules/home/pages/notification-center/targets-table/target-notification-dialog.component.html
  17. 57
      ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-autocomplete.component.html
  18. 42
      ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-autocomplete.component.scss
  19. 213
      ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-autocomplete.component.ts
  20. 13
      ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-notification-dialog.component.html
  21. 16
      ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-notification-dialog.component.scss
  22. 32
      ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-notification-dialog.component.ts
  23. 9
      ui-ngx/src/app/shared/components/entity/entity-list.component.html
  24. 9
      ui-ngx/src/app/shared/components/entity/entity-list.component.ts
  25. 2
      ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.html
  26. 2
      ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.scss
  27. 39
      ui-ngx/src/app/shared/models/notification.models.ts
  28. 53
      ui-ngx/src/assets/locale/locale.constant-en_US.json

8
ui-ngx/src/app/core/http/entity.service.ts

@ -88,6 +88,7 @@ import { WidgetService } from '@core/http/widget.service';
import { DeviceProfileService } from '@core/http/device-profile.service'; import { DeviceProfileService } from '@core/http/device-profile.service';
import { QueueService } from '@core/http/queue.service'; import { QueueService } from '@core/http/queue.service';
import { AssetProfileService } from '@core/http/asset-profile.service'; import { AssetProfileService } from '@core/http/asset-profile.service';
import { NotificationService } from '@core/http/notification.service';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -113,7 +114,8 @@ export class EntityService {
private deviceProfileService: DeviceProfileService, private deviceProfileService: DeviceProfileService,
private assetProfileService: AssetProfileService, private assetProfileService: AssetProfileService,
private utils: UtilsService, private utils: UtilsService,
private queueService: QueueService private queueService: QueueService,
private notificationService: NotificationService
) { } ) { }
private getEntityObservable(entityType: EntityType, entityId: string, private getEntityObservable(entityType: EntityType, entityId: string,
@ -402,6 +404,10 @@ export class EntityService {
pageLink.sortOrder.property = 'title'; pageLink.sortOrder.property = 'title';
entitiesObservable = this.widgetService.getWidgetBundles(pageLink, config); entitiesObservable = this.widgetService.getWidgetBundles(pageLink, config);
break; break;
case EntityType.NOTIFICATION_TARGET:
pageLink.sortOrder.property = 'name';
entitiesObservable = this.notificationService.getNotificationTargets(pageLink, config);
break;
} }
return entitiesObservable; return entitiesObservable;
} }

16
ui-ngx/src/app/core/http/notification.service.ts

@ -23,6 +23,7 @@ import { PageData } from '@shared/models/page/page-data';
import { import {
Notification, Notification,
NotificationRequest, NotificationRequest,
NotificationRequestPreview,
NotificationRule, NotificationRule,
NotificationSettings, NotificationSettings,
NotificationTarget, NotificationTarget,
@ -42,15 +43,16 @@ export class NotificationService {
) { ) {
} }
public getNotifications(pageLink: PageLink, config?: RequestConfig): Observable<PageData<Notification>> { public getNotifications(pageLink: PageLink, unreadOnly = false, config?: RequestConfig): Observable<PageData<Notification>> {
return this.http.get<PageData<Notification>>(`/api/notifications${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); return this.http.get<PageData<Notification>>(`/api/notifications${pageLink.toQuery()}&unreadOnly=${unreadOnly}`,
defaultHttpOptionsFromConfig(config));
} }
public markNotificationAsRead(id: string, config?: RequestConfig): Observable<void> { public markNotificationAsRead(id: string, config?: RequestConfig): Observable<void> {
return this.http.put<void>(`/api/notification/${id}/read`, defaultHttpOptionsFromConfig(config)); return this.http.put<void>(`/api/notification/${id}/read`, defaultHttpOptionsFromConfig(config));
} }
public createNotificationRequest(notification: NotificationRequest, config?: RequestConfig): Observable<any> { public createNotificationRequest(notification: NotificationRequest, config?: RequestConfig): Observable<NotificationRequest> {
return this.http.post<NotificationRequest>('/api/notification/request', notification, defaultHttpOptionsFromConfig(config)); return this.http.post<NotificationRequest>('/api/notification/request', notification, defaultHttpOptionsFromConfig(config));
} }
@ -62,8 +64,14 @@ export class NotificationService {
return this.http.delete<void>(`/api/notification/request/${id}`, defaultHttpOptionsFromConfig(config)); return this.http.delete<void>(`/api/notification/request/${id}`, defaultHttpOptionsFromConfig(config));
} }
public getNotificationRequestPreview(notification: NotificationRequest, config?: RequestConfig): Observable<NotificationRequestPreview> {
return this.http.post<NotificationRequestPreview>('/api/notification/request/preview',
notification, defaultHttpOptionsFromConfig(config));
}
public getNotificationRequests(pageLink: PageLink, config?: RequestConfig): Observable<PageData<NotificationRequest>> { public getNotificationRequests(pageLink: PageLink, config?: RequestConfig): Observable<PageData<NotificationRequest>> {
return this.http.get<PageData<NotificationRequest>>('/api/notification/request/', defaultHttpOptionsFromConfig(config)); return this.http.get<PageData<NotificationRequest>>(`/api/notification/requests${pageLink.toQuery()}`,
defaultHttpOptionsFromConfig(config));
} }
public getNotificationSettings(config?: RequestConfig): Observable<NotificationSettings> { public getNotificationSettings(config?: RequestConfig): Observable<NotificationSettings> {

27
ui-ngx/src/app/modules/home/pages/notification-center/inbox-table/inbox-table-header.component.html

@ -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>

71
ui-ngx/src/app/modules/home/pages/notification-center/inbox-table/inbox-table-header.component.scss

@ -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;
}
}
}

39
ui-ngx/src/app/modules/home/pages/notification-center/inbox-table/inbox-table-header.component.ts

@ -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);
}
}

3
ui-ngx/src/app/modules/home/pages/notification-center/notification-center.component.html

@ -41,6 +41,7 @@
</mat-tab> </mat-tab>
<mat-tab label="{{ 'notification.sent' | translate }}"> <mat-tab label="{{ 'notification.sent' | translate }}">
<tb-notification-table <tb-notification-table
#notificationRequest
[notificationType]="entityType.NOTIFICATION_REQUEST"> [notificationType]="entityType.NOTIFICATION_REQUEST">
</tb-notification-table> </tb-notification-table>
</mat-tab> </mat-tab>
@ -62,7 +63,7 @@
<mat-tab disabled> <mat-tab disabled>
<ng-template mat-tab-label> <ng-template mat-tab-label>
<div fxFlex="100" fxLayout="row" fxLayoutAlign="end center"> <div fxFlex="100" fxLayout="row" fxLayoutAlign="end center">
<button mat-flat-button color="primary"> <button mat-flat-button color="primary" (click)="sendNotification($event)">
<div fxLayout="row" fxLayoutAlign="start center"> <div fxLayout="row" fxLayoutAlign="start center">
<mat-icon>send</mat-icon>{{ 'notification.send-notification' | translate }} <mat-icon>send</mat-icon>{{ 'notification.send-notification' | translate }}
</div> </div>

4
ui-ngx/src/app/modules/home/pages/notification-center/notification-center.component.scss

@ -149,6 +149,10 @@
height: 56px; height: 56px;
} }
.mat-toolbar-single-row {
padding: 0 24px 0 12px;
}
.title-container { .title-container {
width: 100%; width: 100%;
} }

5
ui-ngx/src/app/modules/home/pages/notification-center/notification-center.component.ts

@ -34,6 +34,7 @@ export class NotificationCenterComponent extends PageComponent {
entityType = EntityType; entityType = EntityType;
@ViewChild('matTabGroup', {static: true}) matTabs: MatTabGroup; @ViewChild('matTabGroup', {static: true}) matTabs: MatTabGroup;
@ViewChild('notificationRequest', {static: true}) notificationRequestTable: NotificationTableComponent;
@ViewChildren(NotificationTableComponent) tableComponent: QueryList<NotificationTableComponent>; @ViewChildren(NotificationTableComponent) tableComponent: QueryList<NotificationTableComponent>;
@ -53,4 +54,8 @@ export class NotificationCenterComponent extends PageComponent {
private get currentTableComponent(): NotificationTableComponent { private get currentTableComponent(): NotificationTableComponent {
return this.tableComponent.get(this.matTabs.selectedIndex); return this.tableComponent.get(this.matTabs.selectedIndex);
} }
sendNotification($event: Event) {
this.notificationRequestTable.entityTableConfig.onEntityAction({event: $event, action: 'add', entity: null});
}
} }

12
ui-ngx/src/app/modules/home/pages/notification-center/notification-center.module.ts

@ -35,15 +35,25 @@ import {
import { import {
TemplateTableHeaderComponent TemplateTableHeaderComponent
} from '@home/pages/notification-center/template-table/template-table-header.component'; } from '@home/pages/notification-center/template-table/template-table-header.component';
import {
RequestNotificationDialogComponent
} from '@home/pages/notification-center/request-table/request-notification-dialog.componet';
import {
TemplateAutocompleteComponent
} from '@home/pages/notification-center/template-table/template-autocomplete.component';
import { InboxTableHeaderComponent } from '@home/pages/notification-center/inbox-table/inbox-table-header.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
NotificationCenterComponent, NotificationCenterComponent,
TargetNotificationDialogComponent, TargetNotificationDialogComponent,
TemplateNotificationDialogComponent, TemplateNotificationDialogComponent,
RequestNotificationDialogComponent,
TargetTableHeaderComponent, TargetTableHeaderComponent,
TemplateTableHeaderComponent, TemplateTableHeaderComponent,
NotificationTableComponent NotificationTableComponent,
TemplateAutocompleteComponent,
InboxTableHeaderComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,

81
ui-ngx/src/app/modules/home/pages/notification-center/notification-table/inbox-table-config.ts

@ -15,6 +15,7 @@
/// ///
import { import {
CellActionDescriptor,
DateEntityTableColumn, DateEntityTableColumn,
EntityTableColumn, EntityTableColumn,
EntityTableConfig EntityTableConfig
@ -22,33 +23,95 @@ import {
import { EntityTypeResource } from '@shared/models/entity-type.models'; import { EntityTypeResource } from '@shared/models/entity-type.models';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { Direction } from '@shared/models/page/sort-order'; import { Direction } from '@shared/models/page/sort-order';
import { Notification } from '@shared/models/notification.models'; import {
Notification,
NotificationStatus,
NotificationTemplateTypeTranslateMap
} from '@shared/models/notification.models';
import { NotificationService } from '@core/http/notification.service'; import { NotificationService } from '@core/http/notification.service';
import { InboxTableHeaderComponent } from '@home/pages/notification-center/inbox-table/inbox-table-header.component';
import { TranslateService } from '@ngx-translate/core';
import { take } from 'rxjs/operators';
export class InboxTableConfig extends EntityTableConfig<Notification> { export class InboxTableConfig extends EntityTableConfig<Notification> {
constructor(private notificationService: NotificationService, constructor(private notificationService: NotificationService,
private datePipe: DatePipe, private translate: TranslateService,
updateOnInit = true) { private datePipe: DatePipe) {
super(); super();
this.loadDataOnInit = true;
this.entitiesDeleteEnabled = false; this.entitiesDeleteEnabled = false;
this.entityTranslations = { this.entityTranslations = {
noEntities: 'notification.no-inbox-notification' noEntities: 'notification.no-inbox-notification',
search: 'notification.search-notification'
}; };
this.entityResources = {} as EntityTypeResource<Notification>; this.entityResources = {} as EntityTypeResource<Notification>;
this.entitiesFetchFunction = pageLink => this.notificationService.getNotifications(pageLink); this.entitiesFetchFunction = pageLink => this.notificationService.getNotifications(pageLink, this.componentsData.unreadOnly);
this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC};
this.componentsData = {
unreadOnly: true
};
this.cellActionDescriptors = this.configureCellActions();
this.headerComponent = InboxTableHeaderComponent;
this.headerActionDescriptors = [{
name: this.translate.instant('notification.mark-all-read'),
icon: 'done_all',
isEnabled: () => true,
onAction: $event => this.markAllRead($event)
}];
this.columns.push( this.columns.push(
new DateEntityTableColumn<Notification>('createdTime', 'notification.created-time', this.datePipe, '150px'), new DateEntityTableColumn<Notification>('createdTime', 'notification.created-time', this.datePipe, '150px'),
new EntityTableColumn<Notification>('type', 'notification.type', '10%'), new EntityTableColumn<Notification>('type', 'notification.type', '10%', (notification) =>
new EntityTableColumn<Notification>('text', 'notification.text', '40%'), this.translate.instant(NotificationTemplateTypeTranslateMap.get(notification.type).name)),
new EntityTableColumn<Notification>('info.description', 'notification.description', '50%') new EntityTableColumn<Notification>('subject', 'notification.subject', '30%'),
new EntityTableColumn<Notification>('text', 'notification.text', '60%')
); );
} }
private configureCellActions(): Array<CellActionDescriptor<Notification>> {
return [{
name: this.translate.instant('notification.mark-as-read'),
icon: 'check_circle_outline',
isEnabled: (notification) => notification.status !== NotificationStatus.READ,
onAction: ($event, entity) => this.markAsRead($event, entity)
}];
}
private markAllRead($event: Event) {
if ($event) {
$event.stopPropagation();
}
if (this.componentsData.unreadOnly) {
this.getTable().resetSortAndFilter(true);
} else {
this.updateData();
}
}
private markAsRead($event, entity){
if ($event) {
$event.stopPropagation();
}
this.notificationService.markNotificationAsRead(entity.id.id).subscribe(() => {
if (this.componentsData.unreadOnly) {
this.getTable().dataSource.pageData$.pipe(take(1)).subscribe(
(value) => {
if (value.data.length === 1 && this.getTable().pageLink.page) {
this.getTable().pageLink.page--;
}
this.updateData();
}
);
} else {
this.updateData();
}
});
}
} }

7
ui-ngx/src/app/modules/home/pages/notification-center/notification-table/notification-table.component.ts

@ -25,6 +25,7 @@ import { EntityTableConfig } from '@home/models/entity/entities-table-config.mod
import { InboxTableConfig } from '@home/pages/notification-center/notification-table/inbox-table-config'; import { InboxTableConfig } from '@home/pages/notification-center/notification-table/inbox-table-config';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { TemplateTableConfig } from '@home/pages/notification-center/notification-table/template-table-config'; import { TemplateTableConfig } from '@home/pages/notification-center/notification-table/template-table-config';
import { RequestTableConfig } from '@home/pages/notification-center/notification-table/request-table-config';
@Component({ @Component({
selector: 'tb-notification-table', selector: 'tb-notification-table',
@ -68,6 +69,7 @@ export class NotificationTableComponent implements OnInit {
case EntityType.NOTIFICATION: case EntityType.NOTIFICATION:
return new InboxTableConfig( return new InboxTableConfig(
this.notificationService, this.notificationService,
this.translate,
this.datePipe this.datePipe
); );
case EntityType.NOTIFICATION_TEMPLATE: case EntityType.NOTIFICATION_TEMPLATE:
@ -77,10 +79,11 @@ export class NotificationTableComponent implements OnInit {
this.dialog this.dialog
); );
case EntityType.NOTIFICATION_REQUEST: case EntityType.NOTIFICATION_REQUEST:
return new TemplateTableConfig( return new RequestTableConfig(
this.notificationService, this.notificationService,
this.translate, this.translate,
this.dialog this.dialog,
this.datePipe
); );
} }
} }

143
ui-ngx/src/app/modules/home/pages/notification-center/notification-table/request-table-config.ts

@ -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;
}
}

78
ui-ngx/src/app/modules/home/pages/notification-center/request-table/request-notification-dialog.component.html

@ -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>

81
ui-ngx/src/app/modules/home/pages/notification-center/request-table/request-notification-dialog.component.scss

@ -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;
}
}
}
}

155
ui-ngx/src/app/modules/home/pages/notification-center/request-table/request-notification-dialog.componet.ts

@ -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;
}
});
}
}

3
ui-ngx/src/app/modules/home/pages/notification-center/targets-table/target-notification-dialog.component.html

@ -49,6 +49,9 @@
required required
formControlName="usersIds" formControlName="usersIds"
[entityType]="entityType.USER" [entityType]="entityType.USER"
labelText="user.user-list"
placeholderText="user.user"
requiredText="user.user-list-required"
*ngIf="targetNotificationForm.get('configuration.type').value === notificationTargetConfigType.USER_LIST"> *ngIf="targetNotificationForm.get('configuration.type').value === notificationTargetConfigType.USER_LIST">
</tb-entity-list> </tb-entity-list>
<section <section

57
ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-autocomplete.component.html

@ -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, &apos;...&apos;) }}</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>

42
ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-autocomplete.component.scss

@ -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;
}
}
}
}
}

213
ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-autocomplete.component.ts

@ -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);
}
}

13
ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-notification-dialog.component.html

@ -27,7 +27,7 @@
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar> </mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content [formGroup]="templateNotificationForm"> <div mat-dialog-content>
<mat-horizontal-stepper [linear]="true" labelPosition="end" #addNotificationTemplate [orientation]="(stepperOrientation | async)" <mat-horizontal-stepper [linear]="true" labelPosition="end" #addNotificationTemplate [orientation]="(stepperOrientation | async)"
(selectionChange)="changeStep($event)"> (selectionChange)="changeStep($event)">
<ng-template matStepperIcon="edit"> <ng-template matStepperIcon="edit">
@ -67,14 +67,19 @@
{{ 'notification.message-required' | translate }} {{ 'notification.message-required' | translate }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<label style="margin-bottom: 8px; display: block;">Delivery methods <sup>*</sup></label> <label class="tb-title tb-required"
[ngClass]="{'tb-error': templateNotificationForm.get('configuration.deliveryMethodsTemplates').hasError('atLeastOne')}">
{{ "notification.delivery-methods" | translate }}
</label>
<section formGroupName="deliveryMethodsTemplates" fxLayout="row warap"> <section formGroupName="deliveryMethodsTemplates" fxLayout="row warap">
<section *ngFor="let deliveryMethods of notificationDeliveryMethods; even as isEven" class="delivery-method-container" <section *ngFor="let deliveryMethods of notificationDeliveryMethods; even as isEven" class="delivery-method-container"
[formGroupName]="deliveryMethods" fxFlex="50%" [ngClass]={even:isEven}> [formGroupName]="deliveryMethods" fxFlex="50%" [ngClass]={even:isEven}>
<mat-checkbox class="delivery-method" formControlName="enabled">{{ deliveryMethods }}</mat-checkbox> <mat-slide-toggle class="delivery-method" formControlName="enabled">
{{ notificationDeliveryMethodTranslateMap.get(deliveryMethods) | translate }}
</mat-slide-toggle>
</section> </section>
</section> </section>
<div class="tb-hint">At least one should be selected</div> <div class="tb-hint" translate>notification.at-least-one-should-be-selected</div>
</section> </section>
</form> </form>
</mat-step> </mat-step>

16
ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-notification-dialog.component.scss

@ -1,3 +1,18 @@
/**
* 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"; @import "../../../../../../scss/constants";
:host-context(.tb-fullscreen-dialog .mat-dialog-container) { :host-context(.tb-fullscreen-dialog .mat-dialog-container) {
@ -19,6 +34,7 @@
border: 1px solid rgba(0, 0, 0, 0.12); border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 6px; border-radius: 6px;
width: 100%; width: 100%;
height: 100%;
} }
} }

32
ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-notification-dialog.component.ts

@ -15,11 +15,12 @@
/// ///
import { import {
DeliveryMethodNotificationTemplate,
NotificationDeliveryMethod, NotificationDeliveryMethod,
NotificationDeliveryMethodTranslateMap,
NotificationTemplate, NotificationTemplate,
NotificationTemplateType, NotificationTemplateTypeTranslateMap,
NotificationTemplateTypeTranslateMap, SlackChanelType NotificationType,
SlackChanelType
} from '@shared/models/notification.models'; } from '@shared/models/notification.models';
import { Component, Inject, OnDestroy, ViewChild } from '@angular/core'; import { Component, Inject, OnDestroy, ViewChild } from '@angular/core';
import { DialogComponent } from '@shared/components/dialog.component'; import { DialogComponent } from '@shared/components/dialog.component';
@ -27,7 +28,7 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, ValidationErrors, Validators } from '@angular/forms';
import { NotificationService } from '@core/http/notification.service'; import { NotificationService } from '@core/http/notification.service';
import { deepTrim, isDefined } from '@core/utils'; import { deepTrim, isDefined } from '@core/utils';
import { Observable, Subject } from 'rxjs'; import { Observable, Subject } from 'rxjs';
@ -36,7 +37,6 @@ import { StepperOrientation, StepperSelectionEvent } from '@angular/cdk/stepper'
import { MatStepper } from '@angular/material/stepper'; import { MatStepper } from '@angular/material/stepper';
import { BreakpointObserver } from '@angular/cdk/layout'; import { BreakpointObserver } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants'; import { MediaBreakpoints } from '@shared/models/constants';
import { NotificationType } from '@core/notification/notification.models';
export interface TemplateNotificationDialogData { export interface TemplateNotificationDialogData {
template?: NotificationTemplate; template?: NotificationTemplate;
@ -64,9 +64,10 @@ export class TemplateNotificationDialogComponent
dialogTitle = 'notification.edit-notification-template'; dialogTitle = 'notification.edit-notification-template';
saveButtonLabel = 'action.save'; saveButtonLabel = 'action.save';
notificationTypes = Object.keys(NotificationTemplateType) as NotificationType[]; notificationTypes = Object.keys(NotificationType) as NotificationType[];
notificationDeliveryMethods = Object.keys(NotificationDeliveryMethod) as NotificationDeliveryMethod[]; notificationDeliveryMethods = Object.keys(NotificationDeliveryMethod) as NotificationDeliveryMethod[];
notificationTemplateTypeTranslateMap = NotificationTemplateTypeTranslateMap; notificationTemplateTypeTranslateMap = NotificationTemplateTypeTranslateMap;
notificationDeliveryMethodTranslateMap = NotificationDeliveryMethodTranslateMap;
selectedIndex = 0; selectedIndex = 0;
@ -109,11 +110,11 @@ export class TemplateNotificationDialogComponent
this.templateNotificationForm = this.fb.group({ this.templateNotificationForm = this.fb.group({
name: ['', Validators.required], name: ['', Validators.required],
notificationType: [NotificationTemplateType.GENERAL], notificationType: [NotificationType.GENERAL],
configuration: this.fb.group({ configuration: this.fb.group({
notificationSubject: ['', Validators.required], notificationSubject: ['', Validators.required],
defaultTextTemplate: ['', Validators.required], defaultTextTemplate: ['', Validators.required],
deliveryMethodsTemplates: this.fb.group({}) deliveryMethodsTemplates: this.fb.group({}, {validators: this.atLeastOne()})
}) })
}); });
this.notificationDeliveryMethods.forEach(method => { this.notificationDeliveryMethods.forEach(method => {
@ -190,7 +191,7 @@ export class TemplateNotificationDialogComponent
if (this.selectedIndex === 3 && this.selectedIndex < this.maxStepperIndex && this.smsTemplateForm.pristine) { if (this.selectedIndex === 3 && this.selectedIndex < this.maxStepperIndex && this.smsTemplateForm.pristine) {
return 'action.skip'; return 'action.skip';
} }
if (this.selectedIndex >= this.maxStepperIndex) { if (this.selectedIndex !== 0 && this.selectedIndex >= this.maxStepperIndex) {
return 'action.add'; return 'action.add';
} }
return 'action.next'; return 'action.next';
@ -229,7 +230,7 @@ export class TemplateNotificationDialogComponent
} }
} }
allValid(): boolean { private allValid(): boolean {
return !this.addNotificationTemplate.steps.find((item, index) => { return !this.addNotificationTemplate.steps.find((item, index) => {
if (item.stepControl.invalid) { if (item.stepControl.invalid) {
item.interacted = true; item.interacted = true;
@ -240,4 +241,15 @@ export class TemplateNotificationDialogComponent
} }
}); });
} }
private atLeastOne() {
return (group: FormGroup): ValidationErrors | null => {
let hasAtLeastOne = true;
if (group?.controls) {
const controlsFormValue: FormGroup[] = Object.entries(group.controls).map(method => method[1]) as any;
hasAtLeastOne = controlsFormValue.some(value => value.controls.enabled.value);
}
return hasAtLeastOne ? null : {atLeastOne: true};
};
}
} }

9
ui-ngx/src/app/shared/components/entity/entity-list.component.html

@ -15,8 +15,9 @@
limitations under the License. limitations under the License.
--> -->
<mat-form-field appearance="standard" [formGroup]="entityListFormGroup" class="mat-block"> <mat-form-field appearance="standard" [formGroup]="entityListFormGroup" class="mat-block" floatLabel="always">
<mat-chip-list #chipList formControlName="entities"> <mat-label *ngIf="labelText">{{ labelText | translate }}</mat-label>
<mat-chip-list #chipList formControlName="entities" [required]="required">
<mat-chip <mat-chip
*ngFor="let entity of entities" *ngFor="let entity of entities"
[selectable]="!disabled" [selectable]="!disabled"
@ -25,7 +26,7 @@
{{entity.name}} {{entity.name}}
<mat-icon matChipRemove *ngIf="!disabled">close</mat-icon> <mat-icon matChipRemove *ngIf="!disabled">close</mat-icon>
</mat-chip> </mat-chip>
<input matInput type="text" placeholder="{{ !disabled ? ('entity.entity-list' | translate) : '' }}" <input matInput type="text" placeholder="{{ !disabled ? (placeholderText | translate) : '' }}"
style="max-width: 200px;" style="max-width: 200px;"
#entityInput #entityInput
(focusin)="onFocus()" (focusin)="onFocus()"
@ -49,6 +50,6 @@
</mat-option> </mat-option>
</mat-autocomplete> </mat-autocomplete>
<mat-error *ngIf="entityListFormGroup.get('entities').hasError('required')"> <mat-error *ngIf="entityListFormGroup.get('entities').hasError('required')">
{{ 'entity.entity-list-empty' | translate }} {{ requiredText | translate }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>

9
ui-ngx/src/app/shared/components/entity/entity-list.component.ts

@ -63,6 +63,15 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV
@Input() @Input()
subType: string; subType: string;
@Input()
labelText: string;
@Input()
placeholderText = 'entity.entity-list';
@Input()
requiredText = 'entity.entity-list-empty';
private requiredValue: boolean; private requiredValue: boolean;
get required(): boolean { get required(): boolean {
return this.requiredValue; return this.requiredValue;

2
ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.html

@ -32,7 +32,7 @@
#queueAutocomplete="matAutocomplete" #queueAutocomplete="matAutocomplete"
[displayWith]="displayQueueFn" [displayWith]="displayQueueFn"
> >
<mat-option *ngFor="let queue of filteredQueues | async" [value]="queue" class="queue-option"> <mat-option *ngFor="let queue of filteredQueues | async" [value]="queue" class="template-option">
<span [innerHTML]="queue.name | highlight:searchText"></span> <span [innerHTML]="queue.name | highlight:searchText"></span>
<small class="queue-option-description">{{getDescription(queue)}}</small> <small class="queue-option-description">{{getDescription(queue)}}</small>
</mat-option> </mat-option>

2
ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.scss

@ -15,7 +15,7 @@
*/ */
::ng-deep { ::ng-deep {
.queue-option { .template-option {
.mat-option-text { .mat-option-text {
display: inline; display: inline;
} }

39
ui-ngx/src/app/shared/models/notification.models.ts

@ -29,7 +29,8 @@ export interface Notification {
readonly id: NotificationId; readonly id: NotificationId;
readonly requestId: NotificationRequestId; readonly requestId: NotificationRequestId;
readonly recipientId: UserId; readonly recipientId: UserId;
readonly type: string; readonly type: NotificationType;
readonly subject: string;
readonly text: string; readonly text: string;
readonly info: NotificationInfo; readonly info: NotificationInfo;
readonly originatorType: NotificationOriginatorType; readonly originatorType: NotificationOriginatorType;
@ -44,7 +45,7 @@ export interface NotificationInfo {
export interface NotificationRequest extends Omit<BaseData<NotificationRequestId>, 'label'> { export interface NotificationRequest extends Omit<BaseData<NotificationRequestId>, 'label'> {
tenantId?: TenantId; tenantId?: TenantId;
targets: Array<NotificationTargetId>; targets: Array<string>;
templateId: NotificationTemplateId; templateId: NotificationTemplateId;
info?: NotificationInfo; info?: NotificationInfo;
deliveryMethods: Array<NotificationDeliveryMethod>; deliveryMethods: Array<NotificationDeliveryMethod>;
@ -55,6 +56,12 @@ export interface NotificationRequest extends Omit<BaseData<NotificationRequestId
additionalConfig: NotificationRequestConfig; additionalConfig: NotificationRequestConfig;
} }
export interface NotificationRequestPreview {
totalRecipientsCount: number;
recipientsCountByTarget: { [key in string]: number };
processedTemplates: { [key in NotificationDeliveryMethod]: DeliveryMethodNotificationTemplate };
}
export interface NotificationRequestStats { export interface NotificationRequestStats {
sent: Map<NotificationDeliveryMethod, any>; sent: Map<NotificationDeliveryMethod, any>;
errors: Map<NotificationDeliveryMethod, Map<string, string>>; errors: Map<NotificationDeliveryMethod, Map<string, string>>;
@ -122,7 +129,7 @@ interface CustomerUsersNotificationTargetConfig {
export interface NotificationTemplate extends Omit<BaseData<NotificationTemplateId>, 'label'>{ export interface NotificationTemplate extends Omit<BaseData<NotificationTemplateId>, 'label'>{
tenantId: TenantId; tenantId: TenantId;
notificationType: NotificationTemplateType; notificationType: NotificationType;
configuration: NotificationTemplateConfig; configuration: NotificationTemplateConfig;
} }
@ -174,11 +181,25 @@ export enum NotificationDeliveryMethod {
SLACK = 'SLACK' SLACK = 'SLACK'
} }
export const NotificationDeliveryMethodTranslateMap = new Map<NotificationDeliveryMethod, string>([
[NotificationDeliveryMethod.PUSH, 'notification.delivery-method-type.push'],
[NotificationDeliveryMethod.SMS, 'notification.delivery-method-type.sms'],
[NotificationDeliveryMethod.EMAIL, 'notification.delivery-method-type.email'],
[NotificationDeliveryMethod.SLACK, 'notification.delivery-method-type.slack'],
]);
export enum NotificationRequestStatus { export enum NotificationRequestStatus {
PROCESSED = 'PROCESSED', PROCESSING = 'PROCESSING',
SCHEDULED = 'SCHEDULED' SCHEDULED = 'SCHEDULED',
SENT = 'SENT'
} }
export const NotificationRequestStatusTranslateMap = new Map<NotificationRequestStatus, string>([
[NotificationRequestStatus.PROCESSING, 'notification.requet-status.processing'],
[NotificationRequestStatus.SCHEDULED, 'notification.requet-status.scheduled'],
[NotificationRequestStatus.SENT, 'notification.requet-status.sent']
]);
export enum SlackChanelType { export enum SlackChanelType {
DIRECT= 'DIRECT', DIRECT= 'DIRECT',
PUBLIC_CHANNEL = 'PUBLIC_CHANNEL', PUBLIC_CHANNEL = 'PUBLIC_CHANNEL',
@ -197,7 +218,7 @@ export const NotificationTargetConfigTypeTranslateMap = new Map<NotificationTarg
[NotificationTargetConfigType.CUSTOMER_USERS, 'notification.target-type.customer-users'], [NotificationTargetConfigType.CUSTOMER_USERS, 'notification.target-type.customer-users'],
]); ]);
export enum NotificationTemplateType { export enum NotificationType {
GENERAL = 'GENERAL', GENERAL = 'GENERAL',
ALARM = 'ALARM' ALARM = 'ALARM'
} }
@ -207,14 +228,14 @@ interface NotificationTemplateTypeTranslate {
hint?: string; hint?: string;
} }
export const NotificationTemplateTypeTranslateMap = new Map<NotificationTemplateType, NotificationTemplateTypeTranslate>([ export const NotificationTemplateTypeTranslateMap = new Map<NotificationType, NotificationTemplateTypeTranslate>([
[NotificationTemplateType.GENERAL, [NotificationType.GENERAL,
{ {
name: 'notification.template-type.general', name: 'notification.template-type.general',
hint: 'hint' hint: 'hint'
} }
], ],
[NotificationTemplateType.ALARM, [NotificationType.ALARM,
{ {
name: 'notification.template-type.alarm', name: 'notification.template-type.alarm',
hint: 'hint' hint: 'hint'

53
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -63,7 +63,8 @@
"print": "Print", "print": "Print",
"restore": "Restore", "restore": "Restore",
"confirm": "Confirm", "confirm": "Confirm",
"skip": "Skip" "skip": "Skip",
"send": "Send"
}, },
"aggregation": { "aggregation": {
"aggregation": "Aggregation", "aggregation": "Aggregation",
@ -2672,34 +2673,66 @@
"add-notification-template": "Add notification template", "add-notification-template": "Add notification template",
"add-target": "Add target", "add-target": "Add target",
"add-template": "Add template", "add-template": "Add template",
"at-least-one-should-be-selected": "At least one should be selected",
"all": "All",
"basic-settings": "Basic settings",
"compose": "Compose",
"copy-notification-template": "Copy notification template", "copy-notification-template": "Copy notification template",
"create-target": "Create target", "create-target": "Create target",
"created-time": "Created time", "created-time": "Created time",
"delete-request-text": "Be careful, after the confirmation the notification request will become unrecoverable.",
"delete-request-title": "Are you sure you want to delete the notification request?",
"delete-target-text": "Be careful, after the confirmation the notification target will become unrecoverable.", "delete-target-text": "Be careful, after the confirmation the notification target will become unrecoverable.",
"delete-target-title": "Are you sure you want to delete the notification target '{{targetName}}'?", "delete-target-title": "Are you sure you want to delete the notification target '{{targetName}}'?",
"delete-template-text": "Be careful, after the confirmation the notification template will become unrecoverable.", "delete-template-text": "Be careful, after the confirmation the notification template will become unrecoverable.",
"delete-template-title": "Are you sure you want to delete the notification template '{{templateName}}'?", "delete-template-title": "Are you sure you want to delete the notification template '{{templateName}}'?",
"delivery-method": "Delivery method",
"delivery-methods": "Delivery methods",
"delivery-method-type": {
"email": "Email",
"push": "Web",
"slack": "Slack",
"sms": "SMS"
},
"description": "Description", "description": "Description",
"edit-notification-target": "Edit notification target", "edit-notification-target": "Edit notification target",
"edit-notification-template": "Edit notification template", "edit-notification-template": "Edit notification template",
"email-settings": "Email settings",
"get-customer-id-from-originator": "Get Customer id from originator", "get-customer-id-from-originator": "Get Customer id from originator",
"inbox": "Inbox", "inbox": "Inbox",
"management": "Notification management", "management": "Notification management",
"mark-all-read": "Mark all read",
"mark-as-read": "Mark as read",
"message": "Message", "message": "Message",
"message-required": "Message is required", "message-required": "Message is required",
"new-notification": "New notification",
"no-inbox-notification": "No notification found", "no-inbox-notification": "No notification found",
"no-notification-request": "No notification request",
"no-notification-templates": "No notification templates", "no-notification-templates": "No notification templates",
"no-targets-notification": "No targets notification", "no-targets-notification": "No targets notification",
"notification-center": "Notification center", "notification-center": "Notification center",
"notification-target": "Notification target", "notification-target": "Notification target",
"recipients-count": "{ count, plural, 1 {1 Recipient} other {# Recipients} }",
"requet-status": {
"processing": "Processing",
"scheduled": "Scheduled",
"sent": "Sent"
},
"review": "Review",
"rules": "Rules", "rules": "Rules",
"search-notification": "Search notification",
"search-targets": "Search targets", "search-targets": "Search targets",
"search-templates": "Search templates", "search-templates": "Search templates",
"send-notification": "Send notification", "send-notification": "Send notification",
"send-test-notification": "Send test notification",
"sent": "Sent", "sent": "Sent",
"settings": "Notification settings", "settings": "Notification settings",
"slack-settings": "Slack settings",
"sms-settings": "SMS settings",
"status": "Status",
"subject": "Subject", "subject": "Subject",
"subject-required": "Subject is required", "subject-required": "Subject is required",
"target": "Target",
"target-name": "Name", "target-name": "Name",
"target-name-required": "Name is required", "target-name-required": "Name is required",
"target-type": { "target-type": {
@ -2709,20 +2742,18 @@
"user-list": "User list" "user-list": "User list"
}, },
"targets": "Targets", "targets": "Targets",
"targets-required": "Targets is required",
"template": "Template", "template": "Template",
"template-required": "Template is required",
"template-type": { "template-type": {
"general": "General", "alarm": "Alarm",
"alarm": "Alarm" "general": "General"
}, },
"templates": "Templates", "templates": "Templates",
"text": "Text", "text": "Text",
"type": "Type", "type": "Type",
"basic-settings": "Basic settings", "unread": "Unread",
"send-test-notification": "Send test notification", "web-settings": "Web settings"
"web-settings": "Web settings",
"email-settings": "Email settings",
"sms-settings": "SMS settings",
"slack-settings": "Slack settings"
}, },
"ota-update": { "ota-update": {
"add": "Add package", "add": "Add package",
@ -3498,7 +3529,9 @@
"enable-account-message": "User account was successfully enabled!", "enable-account-message": "User account was successfully enabled!",
"disable-account-message": "User account was successfully disabled!", "disable-account-message": "User account was successfully disabled!",
"copyId": "Copy user Id", "copyId": "Copy user Id",
"idCopiedMessage": "User Id has been copied to clipboard" "idCopiedMessage": "User Id has been copied to clipboard",
"user-list": "User list",
"user-list-required": "User list is required"
}, },
"value": { "value": {
"type": "Value type", "type": "Value type",

Loading…
Cancel
Save