Browse Source

Merge remote-tracking branch 'origin/feature/notification-system' into feature/notification-system

pull/7980/head
ViacheslavKlimov 4 years ago
parent
commit
366b0098a4
  1. 5
      ui-ngx/angular.json
  2. 2
      ui-ngx/package.json
  3. 8
      ui-ngx/src/app/core/http/entity.service.ts
  4. 19
      ui-ngx/src/app/core/http/notification.service.ts
  5. 4
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts
  6. 1
      ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts
  7. 27
      ui-ngx/src/app/modules/home/pages/notification-center/inbox-table/inbox-table-header.component.html
  8. 71
      ui-ngx/src/app/modules/home/pages/notification-center/inbox-table/inbox-table-header.component.scss
  9. 39
      ui-ngx/src/app/modules/home/pages/notification-center/inbox-table/inbox-table-header.component.ts
  10. 3
      ui-ngx/src/app/modules/home/pages/notification-center/notification-center.component.html
  11. 4
      ui-ngx/src/app/modules/home/pages/notification-center/notification-center.component.scss
  12. 5
      ui-ngx/src/app/modules/home/pages/notification-center/notification-center.component.ts
  13. 16
      ui-ngx/src/app/modules/home/pages/notification-center/notification-center.module.ts
  14. 82
      ui-ngx/src/app/modules/home/pages/notification-center/notification-table/inbox-table-config.ts
  15. 7
      ui-ngx/src/app/modules/home/pages/notification-center/notification-table/notification-table.component.ts
  16. 148
      ui-ngx/src/app/modules/home/pages/notification-center/notification-table/request-table-config.ts
  17. 7
      ui-ngx/src/app/modules/home/pages/notification-center/notification-table/template-table-config.ts
  18. 83
      ui-ngx/src/app/modules/home/pages/notification-center/request-table/request-notification-dialog.component.html
  19. 100
      ui-ngx/src/app/modules/home/pages/notification-center/request-table/request-notification-dialog.component.scss
  20. 155
      ui-ngx/src/app/modules/home/pages/notification-center/request-table/request-notification-dialog.componet.ts
  21. 3
      ui-ngx/src/app/modules/home/pages/notification-center/targets-table/target-notification-dialog.component.html
  22. 74
      ui-ngx/src/app/modules/home/pages/notification-center/template-dialog/template-notification-dialog.component.html
  23. 95
      ui-ngx/src/app/modules/home/pages/notification-center/template-dialog/template-notification-dialog.component.ts
  24. 57
      ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-autocomplete.component.html
  25. 36
      ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-autocomplete.component.scss
  26. 213
      ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-autocomplete.component.ts
  27. 159
      ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-notification-dialog.component.html
  28. 101
      ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-notification-dialog.component.scss
  29. 255
      ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-notification-dialog.component.ts
  30. 0
      ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-table-header.component.html
  31. 0
      ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-table-header.component.scss
  32. 0
      ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-table-header.component.ts
  33. 9
      ui-ngx/src/app/shared/components/entity/entity-list.component.html
  34. 9
      ui-ngx/src/app/shared/components/entity/entity-list.component.ts
  35. 2
      ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.html
  36. 2
      ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.scss
  37. 65
      ui-ngx/src/app/shared/models/notification.models.ts
  38. 6
      ui-ngx/src/app/shared/shared.module.ts
  39. 52
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  40. 12
      ui-ngx/yarn.lock

5
ui-ngx/angular.json

@ -49,6 +49,11 @@
"input": "./node_modules/ace-builds/src-noconflict/",
"output": "/"
},
{
"glob": "**/*",
"input": "node_modules/tinymce",
"output": "/assets/tinymce/"
},
{
"glob": "marker-icon-2x.png",
"input": "node_modules/leaflet/dist/images/",

2
ui-ngx/package.json

@ -40,6 +40,7 @@
"@ngrx/store-devtools": "^12.5.1",
"@ngx-translate/core": "^13.0.0",
"@ngx-translate/http-loader": "^6.0.0",
"@tinymce/tinymce-angular": "~4.2.4",
"ace-builds": "1.4.13",
"ace-diff": "^3.0.3",
"angular-gridster2": "~12.1.1",
@ -93,6 +94,7 @@
"split.js": "^1.6.4",
"systemjs": "6.11.0",
"tinycolor2": "^1.4.2",
"tinymce": "~5.10.7",
"tooltipster": "^4.2.8",
"ts-transformer-keys": "^0.4.3",
"tslib": "^2.3.1",

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

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

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

4
ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts

@ -692,4 +692,8 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
this.updateData();
}
}
detectChanges() {
this.cd.markForCheck();
}
}

1
ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts

@ -85,4 +85,5 @@ export interface IEntitiesTableComponent {
cellStyle(entity: BaseData<HasId>, column: EntityColumn<BaseData<HasId>>, row: number): any;
trackByColumnKey(index, column: EntityTableColumn<BaseData<HasId>>): string;
trackByEntityId(index: number, entity: BaseData<HasId>): string;
detectChanges(): void;
}

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 label="{{ 'notification.sent' | translate }}">
<tb-notification-table
#notificationRequest
[notificationType]="entityType.NOTIFICATION_REQUEST">
</tb-notification-table>
</mat-tab>
@ -62,7 +63,7 @@
<mat-tab disabled>
<ng-template mat-tab-label>
<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">
<mat-icon>send</mat-icon>{{ 'notification.send-notification' | translate }}
</div>

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

@ -149,6 +149,10 @@
height: 56px;
}
.mat-toolbar-single-row {
padding: 0 24px 0 12px;
}
.title-container {
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;
@ViewChild('matTabGroup', {static: true}) matTabs: MatTabGroup;
@ViewChild('notificationRequest', {static: true}) notificationRequestTable: NotificationTableComponent;
@ViewChildren(NotificationTableComponent) tableComponent: QueryList<NotificationTableComponent>;
@ -53,4 +54,8 @@ export class NotificationCenterComponent extends PageComponent {
private get currentTableComponent(): NotificationTableComponent {
return this.tableComponent.get(this.matTabs.selectedIndex);
}
sendNotification($event: Event) {
this.notificationRequestTable.entityTableConfig.onEntityAction({event: $event, action: 'add', entity: null});
}
}

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

@ -28,22 +28,32 @@ import {
} from '@home/pages/notification-center/notification-table/notification-table.component';
import {
TemplateNotificationDialogComponent
} from '@home/pages/notification-center/template-dialog/template-notification-dialog.component';
} from '@home/pages/notification-center/template-table/template-notification-dialog.component';
import {
TargetTableHeaderComponent
} from '@home/pages/notification-center/targets-table/target-table-header.component';
import {
TemplateTableHeaderComponent
} from '@home/pages/notification-center/template-dialog/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({
declarations: [
NotificationCenterComponent,
TargetNotificationDialogComponent,
TemplateNotificationDialogComponent,
RequestNotificationDialogComponent,
TargetTableHeaderComponent,
TemplateTableHeaderComponent,
NotificationTableComponent
NotificationTableComponent,
TemplateAutocompleteComponent,
InboxTableHeaderComponent
],
imports: [
CommonModule,

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

@ -15,6 +15,7 @@
///
import {
CellActionDescriptor,
DateEntityTableColumn,
EntityTableColumn,
EntityTableConfig
@ -22,33 +23,96 @@ import {
import { EntityTypeResource } from '@shared/models/entity-type.models';
import { DatePipe } from '@angular/common';
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 { 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> {
constructor(private notificationService: NotificationService,
private datePipe: DatePipe,
updateOnInit = true) {
private translate: TranslateService,
private datePipe: DatePipe) {
super();
this.loadDataOnInit = true;
this.entitiesDeleteEnabled = false;
this.entityTranslations = {
noEntities: 'notification.no-inbox-notification'
noEntities: 'notification.no-inbox-notification',
search: 'notification.search-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.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(
new DateEntityTableColumn<Notification>('createdTime', 'notification.created-time', this.datePipe, '150px'),
new EntityTableColumn<Notification>('type', 'notification.type', '10%'),
new EntityTableColumn<Notification>('text', 'notification.text', '40%'),
new EntityTableColumn<Notification>('info.description', 'notification.description', '50%')
new EntityTableColumn<Notification>('type', 'notification.type', '10%', (notification) =>
this.translate.instant(NotificationTemplateTypeTranslateMap.get(notification.type).name)),
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 {
entity.status = NotificationStatus.READ;
this.getTable().detectChanges();
}
});
}
}

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 { DatePipe } from '@angular/common';
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({
selector: 'tb-notification-table',
@ -68,6 +69,7 @@ export class NotificationTableComponent implements OnInit {
case EntityType.NOTIFICATION:
return new InboxTableConfig(
this.notificationService,
this.translate,
this.datePipe
);
case EntityType.NOTIFICATION_TEMPLATE:
@ -77,10 +79,11 @@ export class NotificationTableComponent implements OnInit {
this.dialog
);
case EntityType.NOTIFICATION_REQUEST:
return new TemplateTableConfig(
return new RequestTableConfig(
this.notificationService,
this.translate,
this.dialog
this.dialog,
this.datePipe
);
}
}

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

@ -0,0 +1,148 @@
///
/// 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 {
NotificationDeliveryMethodTranslateMap,
NotificationRequest,
NotificationRequestInfo,
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';
import { PageLink } from '@shared/models/page/page-link';
export class RequestTableConfig extends EntityTableConfig<NotificationRequest, PageLink, NotificationRequestInfo> {
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<NotificationRequestInfo>('createdTime', 'notification.created-time', this.datePipe, '150px'),
new EntityTableColumn<NotificationRequestInfo>('status', 'notification.status', '15%',
request => this.requestStatus(request.status), request => this.requestStatusStyle(request.status)),
new EntityTableColumn<NotificationRequest>('deliveryMethods', 'notification.delivery-method', '35%',
(request) => request.deliveryMethods
.map((deliveryMethod) => this.translate.instant(NotificationDeliveryMethodTranslateMap.get(deliveryMethod))).join(', '),
() => ({}), false),
new EntityTableColumn<NotificationRequest>('templateName', 'notification.template', '50%')
);
}
private configureCellActions(): Array<CellActionDescriptor<NotificationRequestInfo>> {
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;
}
}

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

@ -28,11 +28,11 @@ import { MatDialog } from '@angular/material/dialog';
import {
TemplateNotificationDialogComponent,
TemplateNotificationDialogData
} from '@home/pages/notification-center/template-dialog/template-notification-dialog.component';
} from '@home/pages/notification-center/template-table/template-notification-dialog.component';
import { TranslateService } from '@ngx-translate/core';
import {
TemplateTableHeaderComponent
} from '@home/pages/notification-center/template-dialog/template-table-header.component';
} from '@home/pages/notification-center/template-table/template-table-header.component';
export class TemplateTableConfig extends EntityTableConfig<NotificationTemplate> {
@ -63,7 +63,8 @@ export class TemplateTableConfig extends EntityTableConfig<NotificationTemplate>
this.columns.push(
new EntityTableColumn<NotificationTemplate>('notificationType', 'notification.type', '15%'),
new EntityTableColumn<NotificationTemplate>('name', 'notification.template', '25%'),
new EntityTableColumn<NotificationTemplate>('subject', 'notification.subject', '25%'),
new EntityTableColumn<NotificationTemplate>('configuration.notificationSubject', 'notification.subject', '25%',
(template) => template.configuration.notificationSubject, () => ({}), false),
new EntityTableColumn<NotificationTemplate>('configuration.defaultTextTemplate', 'notification.message', '35%',
(template) => template.configuration.defaultTextTemplate, () => ({}), false)
);

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

@ -0,0 +1,83 @@
<!--
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>
<div class="details-recipients" *ngIf="notificationRequestForm.get('targets').value.length">
<div *ngFor="let detail of preview.recipientsCountByTarget | keyvalue" class="details-recipient">
<span class="number">{{ detail.value }}</span>{{ detail.key }}
</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>

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

@ -0,0 +1,100 @@
/**
* 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;
}
&> div:not(:last-child) {
margin-bottom: 12px;
}
.details-recipient {
font-size: 14px;
line-height: 14px;
letter-spacing: .2px;
.number {
font-weight: 500;
margin-right: 4px;
}
&:not(:last-of-type) {
margin-bottom: 8px;
}
}
}
@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
formControlName="usersIds"
[entityType]="entityType.USER"
labelText="user.user-list"
placeholderText="user.user"
requiredText="user.user-list-required"
*ngIf="targetNotificationForm.get('configuration.type').value === notificationTargetConfigType.USER_LIST">
</tb-entity-list>
<section

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

@ -1,74 +0,0 @@
<!--
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>{{ dialogTitle | translate }}</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<form mat-dialog-content [formGroup]="templateNotificationForm">
<mat-form-field class="mat-block">
<mat-label translate>notification.target-name</mat-label>
<input matInput formControlName="name" required>
<mat-error *ngIf="templateNotificationForm.get('name').hasError('required')">
{{ 'notification.target-name-required' | translate }}
</mat-error>
</mat-form-field>
<label style="margin-bottom: 8px">{{ 'notification.type' | translate }}<sup> *</sup></label>
<mat-radio-group formControlName="type">
<mat-radio-button [value]="NotificationType.GENERIC">
<div fxLayout="column">
<div>Generic</div>
<div class="tb-hint">hint</div>
</div>
</mat-radio-button>
<mat-radio-button [value]="NotificationType.ALARM">
<div>Alarm</div>
<div class="tb-hint">hint</div>
</mat-radio-button>
</mat-radio-group>
<section formGroupName="configuration">
<mat-form-field class="mat-block">
<mat-label translate>notification.message</mat-label>
<input matInput formControlName="defaultTextTemplate" required>
<mat-error *ngIf="templateNotificationForm.get('configuration.defaultTextTemplate').hasError('required')">
{{ 'notification.message-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>notification.subject</mat-label>
<input matInput required>
</mat-form-field>
</section>
</form>
<mat-divider></mat-divider>
<div mat-dialog-actions fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="end">
<button mat-button
[disabled]="(isLoading$ | async)"
(click)="cancel()">{{ 'action.cancel' | translate }}</button>
<button mat-raised-button
[disabled]="(isLoading$ | async) || templateNotificationForm.invalid || !templateNotificationForm.dirty"
color="primary"
(click)="save()">{{ saveButtonLabel | translate }}</button>
</div>

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

@ -1,95 +0,0 @@
///
/// 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 { NotificationTemplate, NotificationType } from '@shared/models/notification.models';
import { Component, Inject, OnDestroy } 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 { Subject } from 'rxjs';
export interface TemplateNotificationDialogData {
template?: NotificationTemplate;
isAdd?: boolean;
isCopy?: boolean;
}
@Component({
selector: 'tb-template-notification-dialog',
templateUrl: './template-notification-dialog.component.html',
styleUrls: ['./template-notification-dialog.component.scss']
})
export class TemplateNotificationDialogComponent
extends DialogComponent<TemplateNotificationDialogComponent, NotificationTemplate> implements OnDestroy {
templateNotificationForm: FormGroup;
dialogTitle = 'notification.edit-notification-template';
saveButtonLabel = 'action.save';
NotificationType = NotificationType;
private readonly destroy$ = new Subject<void>();
constructor(protected store: Store<AppState>,
protected router: Router,
protected dialogRef: MatDialogRef<TemplateNotificationDialogComponent, NotificationTemplate>,
@Inject(MAT_DIALOG_DATA) public data: TemplateNotificationDialogData,
private fb: FormBuilder,
private notificationService: NotificationService) {
super(store, router, dialogRef);
if (data.isAdd) {
this.dialogTitle = 'notification.add-notification-template';
this.saveButtonLabel = 'action.add';
}
if (data.isCopy) {
this.dialogTitle = 'notification.copy-notification-template';
}
this.templateNotificationForm = this.fb.group({
name: [null, Validators.required],
type: [NotificationType.GENERIC],
configuration: this.fb.group({
defaultTextTemplate: [null, Validators.required]
})
});
}
ngOnDestroy() {
super.ngOnDestroy();
this.destroy$.next();
this.destroy$.complete();
}
cancel(): void {
this.dialogRef.close(null);
}
save() {
let formValue = deepTrim(this.templateNotificationForm.value);
if (isDefined(this.data.template)) {
formValue = Object.assign({}, this.data.template, formValue);
}
this.notificationService.saveNotificationTemplate(formValue).subscribe(
(target) => this.dialogRef.close(target)
);
}
}

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>

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

@ -13,27 +13,29 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
width: 600px;
.tb-hint {
padding: 0;
}
}
::ng-deep {
.template-option {
.mat-option-text {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 4px;
}
:host ::ng-deep {
.mat-radio-group {
display: flex;
flex-direction: row;
.template-option-name {
margin-right: 8px;
}
.mat-radio-button {
flex: 1 1 100%;
padding: 14px;
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 6px;
.mat-chip {
&.mat-standard-chip {
padding: 4px 8px;
border-radius: 14px;
min-height: 28px;
&:not(:last-child){
margin-right: 8px;
&.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);
}
}

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

@ -0,0 +1,159 @@
<!--
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>{{ dialogTitle | translate }}</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<mat-horizontal-stepper [linear]="true" labelPosition="end" #addNotificationTemplate [orientation]="(stepperOrientation | async)"
(selectionChange)="changeStep($event)">
<ng-template matStepperIcon="edit">
<mat-icon>check</mat-icon>
</ng-template>
<mat-step [stepControl]="templateNotificationForm">
<ng-template matStepLabel>{{ 'notification.basic-settings' | translate }}</ng-template>
<form [formGroup]="templateNotificationForm" style="padding-bottom: 16px;">
<mat-form-field class="mat-block">
<mat-label translate>notification.target-name</mat-label>
<input matInput formControlName="name" required>
<mat-error *ngIf="templateNotificationForm.get('name').hasError('required')">
{{ 'notification.target-name-required' | translate }}
</mat-error>
</mat-form-field>
<label style="margin-bottom: 8px">{{ 'notification.type' | translate }}</label>
<mat-radio-group formControlName="notificationType">
<mat-radio-button *ngFor="let notificationType of notificationTypes" [value]="notificationType">
<div fxLayout="column">
<div>{{ notificationTemplateTypeTranslateMap.get(notificationType).name | translate }}</div>
<div class="tb-hint">{{ notificationTemplateTypeTranslateMap.get(notificationType).hint }}</div>
</div>
</mat-radio-button>
</mat-radio-group>
<section formGroupName="configuration">
<mat-form-field class="mat-block">
<mat-label translate>notification.subject</mat-label>
<input matInput formControlName="notificationSubject" required>
<mat-error *ngIf="templateNotificationForm.get('configuration.notificationSubject').hasError('required')">
{{ 'notification.subject-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>notification.message</mat-label>
<input matInput formControlName="defaultTextTemplate" required>
<mat-error *ngIf="templateNotificationForm.get('configuration.defaultTextTemplate').hasError('required')">
{{ 'notification.message-required' | translate }}
</mat-error>
</mat-form-field>
<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 *ngFor="let deliveryMethods of notificationDeliveryMethods; even as isEven" class="delivery-method-container"
[formGroupName]="deliveryMethods" fxFlex="50%" [ngClass]={even:isEven}>
<mat-slide-toggle class="delivery-method" formControlName="enabled">
{{ notificationDeliveryMethodTranslateMap.get(deliveryMethods) | translate }}
</mat-slide-toggle>
</section>
</section>
<div class="tb-hint" translate>notification.at-least-one-should-be-selected</div>
</section>
</form>
</mat-step>
<mat-step optional *ngIf="templateNotificationForm.get('configuration.deliveryMethodsTemplates.PUSH.enabled').value"
[stepControl]="pushTemplateForm">
<ng-template matStepLabel>{{ 'notification.web-settings' | translate }}</ng-template>
<form [formGroup]="pushTemplateForm">
<mat-form-field class="mat-block" floatLabel="always">
<mat-label translate>notification.subject</mat-label>
<input matInput formControlName="subject" [placeholder]="templateNotificationForm.get('configuration.notificationSubject').value">
</mat-form-field>
<mat-form-field class="mat-block" floatLabel="always">
<mat-label translate>notification.message</mat-label>
<input matInput formControlName="body" [placeholder]="templateNotificationForm.get('configuration.defaultTextTemplate').value">
</mat-form-field>
</form>
</mat-step>
<mat-step optional *ngIf="templateNotificationForm.get('configuration.deliveryMethodsTemplates.EMAIL.enabled').value"
[stepControl]="emailTemplateForm">
<ng-template matStepLabel>{{ 'notification.email-settings' | translate }}</ng-template>
<ng-template matStepContent>
<form [formGroup]="emailTemplateForm">
<mat-form-field class="mat-block" floatLabel="always">
<mat-label translate>notification.subject</mat-label>
<input matInput formControlName="subject" [placeholder]="templateNotificationForm.get('configuration.notificationSubject').value">
</mat-form-field>
<mat-label class="tb-title" translate>notification.message</mat-label>
<editor [init]="tinyMceOptions" formControlName="body"></editor>
</form>
</ng-template>
</mat-step>
<mat-step optional *ngIf="templateNotificationForm.get('configuration.deliveryMethodsTemplates.SMS.enabled').value"
[stepControl]="smsTemplateForm">
<ng-template matStepLabel>{{ 'notification.sms-settings' | translate }}</ng-template>
<form [formGroup]="smsTemplateForm">
<mat-form-field class="mat-block" floatLabel="always">
<mat-label translate>notification.message</mat-label>
<input matInput formControlName="body" [placeholder]="templateNotificationForm.get('configuration.defaultTextTemplate').value">
</mat-form-field>
</form>
</mat-step>
<mat-step optional *ngIf="templateNotificationForm.get('configuration.deliveryMethodsTemplates.SLACK.enabled').value"
[stepControl]="slackTemplateForm">
<ng-template matStepLabel>{{ 'notification.slack-settings' | translate }}</ng-template>
<form [formGroup]="slackTemplateForm">
<mat-form-field class="mat-block" floatLabel="always">
<mat-label translate>notification.message</mat-label>
<input matInput formControlName="body" [placeholder]="templateNotificationForm.get('configuration.defaultTextTemplate').value">
</mat-form-field>
<mat-form-field class="mat-block" floatLabel="always">
<mat-label>Conversation type</mat-label>
<input matInput formControlName="conversationType">
</mat-form-field>
<mat-form-field class="mat-block" floatLabel="always">
<mat-label>Conversation id</mat-label>
<input matInput formControlName="conversationId" required>
<mat-error *ngIf="slackTemplateForm.get('conversationId').hasError('required')">
{{ 'notification.subject-required' | translate }}
</mat-error>
</mat-form-field>
</form>
</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 *ngIf="selectedIndex > 0"
mat-button mat-stroked-button color="primary">
{{ 'notification.send-test-notification' | translate }}
</button>
<button mat-raised-button
color="primary"
(click)="nextStep()">{{ nextStepLabel() | translate }}</button>
</div>

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

@ -0,0 +1,101 @@
/**
* 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: 720px;
.tb-hint {
padding: 0;
}
.delivery-method-container {
padding-bottom: 8px;
&.even {
padding-right: 8px;
}
.delivery-method {
padding: 16px 12px;
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 6px;
width: 100%;
height: 100%;
}
}
@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: 500px;
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;
}
}
}
.mat-radio-group {
display: flex;
flex-direction: row;
.mat-radio-button {
flex: 1 1 100%;
padding: 14px;
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 6px;
&:not(:last-child){
margin-right: 8px;
}
}
}
}

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

@ -0,0 +1,255 @@
///
/// 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 {
NotificationDeliveryMethod,
NotificationDeliveryMethodTranslateMap,
NotificationTemplate,
NotificationTemplateTypeTranslateMap,
NotificationType,
SlackChanelType
} 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, ValidationErrors, Validators } from '@angular/forms';
import { NotificationService } from '@core/http/notification.service';
import { deepTrim, isDefined } from '@core/utils';
import { Observable, Subject } from 'rxjs';
import { map } from 'rxjs/operators';
import { StepperOrientation, StepperSelectionEvent } from '@angular/cdk/stepper';
import { MatStepper } from '@angular/material/stepper';
import { BreakpointObserver } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants';
export interface TemplateNotificationDialogData {
template?: NotificationTemplate;
isAdd?: boolean;
isCopy?: boolean;
}
@Component({
selector: 'tb-template-notification-dialog',
templateUrl: './template-notification-dialog.component.html',
styleUrls: ['./template-notification-dialog.component.scss']
})
export class TemplateNotificationDialogComponent
extends DialogComponent<TemplateNotificationDialogComponent, NotificationTemplate> implements OnDestroy {
@ViewChild('addNotificationTemplate', {static: true}) addNotificationTemplate: MatStepper;
stepperOrientation: Observable<StepperOrientation>;
templateNotificationForm: FormGroup;
pushTemplateForm: FormGroup;
emailTemplateForm: FormGroup;
smsTemplateForm: FormGroup;
slackTemplateForm: FormGroup;
dialogTitle = 'notification.edit-notification-template';
saveButtonLabel = 'action.save';
notificationTypes = Object.keys(NotificationType) as NotificationType[];
notificationDeliveryMethods = Object.keys(NotificationDeliveryMethod) as NotificationDeliveryMethod[];
notificationTemplateTypeTranslateMap = NotificationTemplateTypeTranslateMap;
notificationDeliveryMethodTranslateMap = NotificationDeliveryMethodTranslateMap;
selectedIndex = 0;
tinyMceOptions: Record<string, any>;
private readonly destroy$ = new Subject<void>();
constructor(protected store: Store<AppState>,
protected router: Router,
protected dialogRef: MatDialogRef<TemplateNotificationDialogComponent, NotificationTemplate>,
@Inject(MAT_DIALOG_DATA) public data: TemplateNotificationDialogData,
private breakpointObserver: BreakpointObserver,
private fb: FormBuilder,
private notificationService: NotificationService) {
super(store, router, dialogRef);
if (data.isAdd) {
this.dialogTitle = 'notification.add-notification-template';
this.saveButtonLabel = 'action.add';
}
if (data.isCopy) {
this.dialogTitle = 'notification.copy-notification-template';
}
this.tinyMceOptions = {
base_url: '/assets/tinymce',
suffix: '.min',
plugins: ['link table image imagetools code fullscreen'],
menubar: 'edit insert tools view format table',
toolbar: 'fontselect fontsizeselect | formatselect | bold italic strikethrough forecolor backcolor ' +
'| link | table | image | alignleft aligncenter alignright alignjustify ' +
'| numlist bullist outdent indent | removeformat | code | fullscreen',
height: 400,
autofocus: false,
branding: false
};
this.stepperOrientation = this.breakpointObserver.observe(MediaBreakpoints['gt-xs'])
.pipe(map(({matches}) => matches ? 'horizontal' : 'vertical'));
this.templateNotificationForm = this.fb.group({
name: ['', Validators.required],
notificationType: [NotificationType.GENERAL],
configuration: this.fb.group({
notificationSubject: ['', Validators.required],
defaultTextTemplate: ['', Validators.required],
deliveryMethodsTemplates: this.fb.group({}, {validators: this.atLeastOne()})
})
});
this.notificationDeliveryMethods.forEach(method => {
(this.templateNotificationForm.get('configuration.deliveryMethodsTemplates') as FormGroup)
.addControl(method, this.fb.group({method, enabled: method === NotificationDeliveryMethod.PUSH}), {emitEvent: false});
});
this.pushTemplateForm = this.fb.group({
subject: [''],
body: [''],
icon: [''],
actionButtonConfig: ['']
});
this.emailTemplateForm = this.fb.group({
subject: [''],
body: ['']
});
this.smsTemplateForm = this.fb.group({
body: ['']
});
this.slackTemplateForm = this.fb.group({
body: [''],
conversationId: ['', Validators.required],
conversationType: [SlackChanelType.DIRECT]
});
}
ngOnDestroy() {
super.ngOnDestroy();
this.destroy$.next();
this.destroy$.complete();
}
cancel(): void {
this.dialogRef.close(null);
}
save() {
let formValue = deepTrim(this.templateNotificationForm.value);
if (isDefined(this.data.template)) {
formValue = Object.assign({}, this.data.template, formValue);
}
this.notificationService.saveNotificationTemplate(formValue).subscribe(
(target) => this.dialogRef.close(target)
);
}
changeStep($event: StepperSelectionEvent) {
this.selectedIndex = $event.selectedIndex;
}
backStep() {
this.addNotificationTemplate.previous();
}
nextStep() {
if (this.selectedIndex >= this.maxStepperIndex) {
this.add();
} else {
this.addNotificationTemplate.next();
}
}
nextStepLabel(): string {
if (this.selectedIndex === 1 && this.selectedIndex < this.maxStepperIndex && this.pushTemplateForm.pristine) {
return 'action.skip';
}
if (this.selectedIndex === 2 && this.selectedIndex < this.maxStepperIndex && this.emailTemplateForm.pristine) {
return 'action.skip';
}
if (this.selectedIndex === 3 && this.selectedIndex < this.maxStepperIndex && this.smsTemplateForm.pristine) {
return 'action.skip';
}
if (this.selectedIndex !== 0 && this.selectedIndex >= this.maxStepperIndex) {
return 'action.add';
}
return 'action.next';
}
private get maxStepperIndex(): number {
return this.addNotificationTemplate?._steps?.length - 1;
}
private add(): void {
if (this.allValid()) {
const formValue: NotificationTemplate = this.templateNotificationForm.value;
if (formValue.configuration.deliveryMethodsTemplates.PUSH.enabled) {
Object.assign(formValue.configuration.deliveryMethodsTemplates.PUSH, this.pushTemplateForm.value);
} else {
delete formValue.configuration.deliveryMethodsTemplates.PUSH;
}
if (formValue.configuration.deliveryMethodsTemplates.EMAIL.enabled) {
Object.assign(formValue.configuration.deliveryMethodsTemplates.EMAIL, this.emailTemplateForm.value);
} else {
delete formValue.configuration.deliveryMethodsTemplates.EMAIL;
}
if (formValue.configuration.deliveryMethodsTemplates.SMS.enabled) {
Object.assign(formValue.configuration.deliveryMethodsTemplates.SMS, this.smsTemplateForm.value);
} else {
delete formValue.configuration.deliveryMethodsTemplates.SMS;
}
if (formValue.configuration.deliveryMethodsTemplates.SLACK.enabled) {
Object.assign(formValue.configuration.deliveryMethodsTemplates.SLACK, this.slackTemplateForm.value);
} else {
delete formValue.configuration.deliveryMethodsTemplates.SLACK;
}
this.notificationService.saveNotificationTemplate(deepTrim(formValue)).subscribe(
(target) => this.dialogRef.close(target)
);
}
}
private allValid(): boolean {
return !this.addNotificationTemplate.steps.find((item, index) => {
if (item.stepControl.invalid) {
item.interacted = true;
this.addNotificationTemplate.selectedIndex = index;
return true;
} else {
return false;
}
});
}
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};
};
}
}

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

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

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

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

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

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

@ -32,7 +32,7 @@
#queueAutocomplete="matAutocomplete"
[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>
<small class="queue-option-description">{{getDescription(queue)}}</small>
</mat-option>

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

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

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

@ -29,7 +29,8 @@ export interface Notification {
readonly id: NotificationId;
readonly requestId: NotificationRequestId;
readonly recipientId: UserId;
readonly type: string;
readonly type: NotificationType;
readonly subject: string;
readonly text: string;
readonly info: NotificationInfo;
readonly originatorType: NotificationOriginatorType;
@ -44,7 +45,7 @@ export interface NotificationInfo {
export interface NotificationRequest extends Omit<BaseData<NotificationRequestId>, 'label'> {
tenantId?: TenantId;
targets: Array<NotificationTargetId>;
targets: Array<string>;
templateId: NotificationTemplateId;
info?: NotificationInfo;
deliveryMethods: Array<NotificationDeliveryMethod>;
@ -55,6 +56,17 @@ export interface NotificationRequest extends Omit<BaseData<NotificationRequestId
additionalConfig: NotificationRequestConfig;
}
export interface NotificationRequestInfo extends NotificationRequest {
templateName: string;
deliveryMethods: NotificationDeliveryMethod[];
}
export interface NotificationRequestPreview {
totalRecipientsCount: number;
recipientsCountByTarget: { [key in string]: number };
processedTemplates: { [key in NotificationDeliveryMethod]: DeliveryMethodNotificationTemplate };
}
export interface NotificationRequestStats {
sent: Map<NotificationDeliveryMethod, any>;
errors: Map<NotificationDeliveryMethod, Map<string, string>>;
@ -128,12 +140,16 @@ export interface NotificationTemplate extends Omit<BaseData<NotificationTemplate
interface NotificationTemplateConfig {
defaultTextTemplate: string;
deliveryMethodsTemplates: Map<NotificationDeliveryMethod, DeliveryMethodNotificationTemplate>;
notificationSubject: string;
deliveryMethodsTemplates: {
[key in NotificationDeliveryMethod]: DeliveryMethodNotificationTemplate
};
}
interface DeliveryMethodNotificationTemplate extends
export interface DeliveryMethodNotificationTemplate extends
Partial<PushDeliveryMethodNotificationTemplate & EmailDeliveryMethodNotificationTemplate & SlackDeliveryMethodNotificationTemplate>{
body?: string;
enabled: boolean;
method: NotificationDeliveryMethod;
}
@ -170,13 +186,27 @@ export enum NotificationDeliveryMethod {
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 {
PROCESSED = 'PROCESSED',
SCHEDULED = 'SCHEDULED'
PROCESSING = 'PROCESSING',
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 {
USER= 'USER',
DIRECT= 'DIRECT',
PUBLIC_CHANNEL = 'PUBLIC_CHANNEL',
PRIVATE_CHANNEL = 'PRIVATE_CHANNEL'
}
@ -194,6 +224,25 @@ export const NotificationTargetConfigTypeTranslateMap = new Map<NotificationTarg
]);
export enum NotificationType {
GENERIC = 'GENERIC',
GENERAL = 'GENERAL',
ALARM = 'ALARM'
}
interface NotificationTemplateTypeTranslate {
name: string;
hint?: string;
}
export const NotificationTemplateTypeTranslateMap = new Map<NotificationType, NotificationTemplateTypeTranslate>([
[NotificationType.GENERAL,
{
name: 'notification.template-type.general',
hint: 'hint'
}
],
[NotificationType.ALARM,
{
name: 'notification.template-type.alarm',
hint: 'hint'
}]
]);

6
ui-ngx/src/app/shared/shared.module.ts

@ -64,6 +64,7 @@ import { ShareModule as ShareButtonsModule } from 'ngx-sharebuttons';
import { HotkeyModule } from 'angular2-hotkeys';
import { ColorPickerModule } from 'ngx-color-picker';
import { NgxHmCarouselModule } from 'ngx-hm-carousel';
import { EditorModule, TINYMCE_SCRIPT_SRC } from '@tinymce/tinymce-angular';
import { UserMenuComponent } from '@shared/components/user-menu.component';
import { NospacePipe } from '@shared/pipe/nospace.pipe';
import { TranslateModule } from '@ngx-translate/core';
@ -188,6 +189,10 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
provide: FlowInjectionToken,
useValue: Flow
},
{
provide: TINYMCE_SCRIPT_SRC,
useValue: 'assets/tinymce/tinymce.min.js'
},
{
provide: MAT_DATE_LOCALE,
useValue: 'en-GB'
@ -457,6 +462,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
HotkeyModule,
ColorPickerModule,
NgxHmCarouselModule,
EditorModule,
DndModule,
NgxFlowchartModule,
MarkdownModule,

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

@ -55,13 +55,16 @@
"continue": "Continue",
"discard-changes": "Discard Changes",
"download": "Download",
"next": "Next",
"next-with-label": "Next: {{label}}",
"read-more": "Read more",
"hide": "Hide",
"done": "Done",
"print": "Print",
"restore": "Restore",
"confirm": "Confirm"
"confirm": "Confirm",
"skip": "Skip",
"send": "Send"
},
"aggregation": {
"aggregation": "Aggregation",
@ -2670,33 +2673,66 @@
"add-notification-template": "Add notification template",
"add-target": "Add target",
"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",
"create-target": "Create target",
"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-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-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",
"edit-notification-target": "Edit notification target",
"edit-notification-template": "Edit notification template",
"email-settings": "Email settings",
"get-customer-id-from-originator": "Get Customer id from originator",
"inbox": "Inbox",
"management": "Notification management",
"mark-all-read": "Mark all read",
"mark-as-read": "Mark as read",
"message": "Message",
"message-required": "Message is required",
"new-notification": "New notification",
"no-inbox-notification": "No notification found",
"no-notification-request": "No notification request",
"no-notification-templates": "No notification templates",
"no-targets-notification": "No targets notification",
"notification-center": "Notification center",
"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",
"search-notification": "Search notification",
"search-targets": "Search targets",
"search-templates": "Search templates",
"send-notification": "Send notification",
"send-test-notification": "Send test notification",
"sent": "Sent",
"settings": "Notification settings",
"slack-settings": "Slack settings",
"sms-settings": "SMS settings",
"status": "Status",
"subject": "Subject",
"subject-required": "Subject is required",
"target": "Target",
"target-name": "Name",
"target-name-required": "Name is required",
"target-type": {
@ -2706,10 +2742,18 @@
"user-list": "User list"
},
"targets": "Targets",
"targets-required": "Targets is required",
"template": "Template",
"template-required": "Template is required",
"template-type": {
"alarm": "Alarm",
"general": "General"
},
"templates": "Templates",
"text": "Text",
"type": "Type"
"type": "Type",
"unread": "Unread",
"web-settings": "Web settings"
},
"ota-update": {
"add": "Add package",
@ -3485,7 +3529,9 @@
"enable-account-message": "User account was successfully enabled!",
"disable-account-message": "User account was successfully disabled!",
"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": {
"type": "Value type",

12
ui-ngx/yarn.lock

@ -1641,6 +1641,13 @@
"@angular-devkit/schematics" "12.2.17"
jsonc-parser "3.0.0"
"@tinymce/tinymce-angular@~4.2.4":
version "4.2.4"
resolved "https://registry.yarnpkg.com/@tinymce/tinymce-angular/-/tinymce-angular-4.2.4.tgz#45974f39fcdb1af6f30dba6405bfb0b8a3db748f"
integrity sha512-A+FIvhTahHaqDuvcXovuhzvpCR2yjUbPuF+Z/t/MnNaCv/9Y0w7W5Qgmo5wARIW+NM7o8msWcMSxOMK4xkv10Q==
dependencies:
tslib "^1.10.0"
"@tootallnate/once@1":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
@ -9582,6 +9589,11 @@ tinycolor2@^1.4.2:
resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803"
integrity sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==
tinymce@~5.10.7:
version "5.10.7"
resolved "https://registry.yarnpkg.com/tinymce/-/tinymce-5.10.7.tgz#d89d446f1962f2a1df6b2b70018ce475ec7ffb80"
integrity sha512-9UUjaO0R7FxcFo0oxnd1lMs7H+D0Eh+dDVo5hKbVe1a+VB0nit97vOqlinj+YwgoBDt6/DSCUoWqAYlLI8BLYA==
tmp@0.0.30:
version "0.0.30"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.30.tgz#72419d4a8be7d6ce75148fd8b324e593a711c2ed"

Loading…
Cancel
Save