diff --git a/ui-ngx/angular.json b/ui-ngx/angular.json index b844d46e24..7d52f9107f 100644 --- a/ui-ngx/angular.json +++ b/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/", diff --git a/ui-ngx/package.json b/ui-ngx/package.json index 6b8112adad..b163dfa77b 100644 --- a/ui-ngx/package.json +++ b/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", diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index d4cd4ad24f..e3ee10bbc4 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/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; } diff --git a/ui-ngx/src/app/core/http/notification.service.ts b/ui-ngx/src/app/core/http/notification.service.ts index 93e6f0ab22..a83aa3c998 100644 --- a/ui-ngx/src/app/core/http/notification.service.ts +++ b/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> { - return this.http.get>(`/api/notifications${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + public getNotifications(pageLink: PageLink, unreadOnly = false, config?: RequestConfig): Observable> { + return this.http.get>(`/api/notifications${pageLink.toQuery()}&unreadOnly=${unreadOnly}`, + defaultHttpOptionsFromConfig(config)); } public markNotificationAsRead(id: string, config?: RequestConfig): Observable { return this.http.put(`/api/notification/${id}/read`, defaultHttpOptionsFromConfig(config)); } - public createNotificationRequest(notification: NotificationRequest, config?: RequestConfig): Observable { + public createNotificationRequest(notification: NotificationRequest, config?: RequestConfig): Observable { return this.http.post('/api/notification/request', notification, defaultHttpOptionsFromConfig(config)); } @@ -62,8 +65,14 @@ export class NotificationService { return this.http.delete(`/api/notification/request/${id}`, defaultHttpOptionsFromConfig(config)); } - public getNotificationRequests(pageLink: PageLink, config?: RequestConfig): Observable> { - return this.http.get>('/api/notification/request/', defaultHttpOptionsFromConfig(config)); + public getNotificationRequestPreview(notification: NotificationRequest, config?: RequestConfig): Observable { + return this.http.post('/api/notification/request/preview', + notification, defaultHttpOptionsFromConfig(config)); + } + + public getNotificationRequests(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/notification/requests${pageLink.toQuery()}`, + defaultHttpOptionsFromConfig(config)); } public getNotificationSettings(config?: RequestConfig): Observable { diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts index 074381f5de..618901c174 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts +++ b/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(); + } } diff --git a/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts b/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts index 0b0789aaf2..39ecad3954 100644 --- a/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts @@ -85,4 +85,5 @@ export interface IEntitiesTableComponent { cellStyle(entity: BaseData, column: EntityColumn>, row: number): any; trackByColumnKey(index, column: EntityTableColumn>): string; trackByEntityId(index: number, entity: BaseData): string; + detectChanges(): void; } diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/inbox-table/inbox-table-header.component.html b/ui-ngx/src/app/modules/home/pages/notification-center/inbox-table/inbox-table-header.component.html new file mode 100644 index 0000000000..aa34aed8ff --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification-center/inbox-table/inbox-table-header.component.html @@ -0,0 +1,27 @@ + +
+ + Unread + {{ 'notification.all' | translate }} + + +
diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/inbox-table/inbox-table-header.component.scss b/ui-ngx/src/app/modules/home/pages/notification-center/inbox-table/inbox-table-header.component.scss new file mode 100644 index 0000000000..69b772f491 --- /dev/null +++ b/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; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/inbox-table/inbox-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/notification-center/inbox-table/inbox-table-header.component.ts new file mode 100644 index 0000000000..32a41fdadf --- /dev/null +++ b/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 { + + constructor(protected store: Store) { + super(store); + } + + changeUnreadOnly(unreadOnly: boolean) { + this.entitiesTableConfig.componentsData.unreadOnly = unreadOnly; + this.entitiesTableConfig.getTable().resetSortAndFilter(true); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/notification-center.component.html b/ui-ngx/src/app/modules/home/pages/notification-center/notification-center.component.html index fb338ec54d..b81aa6cee6 100644 --- a/ui-ngx/src/app/modules/home/pages/notification-center/notification-center.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification-center/notification-center.component.html @@ -41,6 +41,7 @@ @@ -62,7 +63,7 @@
- + + + +
+
+ + + check + + + {{ 'notification.compose' | translate }} +
+ + + + +
+
+ + {{ 'notification.review' | translate }} + + +
+
+
+ supervisor_account +
{{ 'notification.recipients-count' | translate : {count: preview.totalRecipientsCount} }}
+
+
+
+ {{ detail.value }}{{ detail.key }} +
+
+
+
+
+
+
+ +
+ + + +
diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/request-table/request-notification-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/notification-center/request-table/request-notification-dialog.component.scss new file mode 100644 index 0000000000..54aeea8bc4 --- /dev/null +++ b/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; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/request-table/request-notification-dialog.componet.ts b/ui-ngx/src/app/modules/home/pages/notification-center/request-table/request-notification-dialog.componet.ts new file mode 100644 index 0000000000..53e6078a9e --- /dev/null +++ b/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 implements OnDestroy { + + @ViewChild('createNotification', {static: true}) createNotification: MatStepper; + stepperOrientation: Observable; + isAdd = true; + entityType = EntityType; + notificationRequestForm: FormGroup; + + selectedIndex = 0; + preview: NotificationRequestPreview = null; + + private readonly destroy$ = new Subject(); + + constructor(protected store: Store, + protected router: Router, + protected dialogRef: MatDialogRef, + @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; + } + }); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/targets-table/target-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification-center/targets-table/target-notification-dialog.component.html index 5611cc278a..7d03645d31 100644 --- a/ui-ngx/src/app/modules/home/pages/notification-center/targets-table/target-notification-dialog.component.html +++ b/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">
- -

{{ dialogTitle | translate }}

- - -
- - -
-
- - notification.target-name - - - {{ 'notification.target-name-required' | translate }} - - - - - -
-
Generic
-
hint
-
-
- -
Alarm
-
hint
-
-
-
- - notification.message - - - {{ 'notification.message-required' | translate }} - - - - notification.subject - - -
-
- -
- - -
diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/template-dialog/template-notification-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/notification-center/template-dialog/template-notification-dialog.component.ts deleted file mode 100644 index b115544ce5..0000000000 --- a/ui-ngx/src/app/modules/home/pages/notification-center/template-dialog/template-notification-dialog.component.ts +++ /dev/null @@ -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 implements OnDestroy { - - templateNotificationForm: FormGroup; - dialogTitle = 'notification.edit-notification-template'; - saveButtonLabel = 'action.save'; - - NotificationType = NotificationType; - - private readonly destroy$ = new Subject(); - - constructor(protected store: Store, - protected router: Router, - protected dialogRef: MatDialogRef, - @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) - ); - } -} diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-autocomplete.component.html b/ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-autocomplete.component.html new file mode 100644 index 0000000000..0c2a692cc7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-autocomplete.component.html @@ -0,0 +1,57 @@ + + + + + + + + + + {{ notificationDeliveryMethodTranslateMap.get(method.key) | translate }} + + + + +
+
+ notification.no-notification-templates +
+ + Not template matching {{ truncate.transform(searchText, true, 6, '...') }} + +
+
+
+ + {{ 'notification.template-required' | translate }} + +
diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/template-dialog/template-notification-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-autocomplete.component.scss similarity index 61% rename from ui-ngx/src/app/modules/home/pages/notification-center/template-dialog/template-notification-dialog.component.scss rename to ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-autocomplete.component.scss index 61059075f6..e0ff2f406a 100644 --- a/ui-ngx/src/app/modules/home/pages/notification-center/template-dialog/template-notification-dialog.component.scss +++ b/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; + } } } } diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-autocomplete.component.ts b/ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-autocomplete.component.ts new file mode 100644 index 0000000000..b478718758 --- /dev/null +++ b/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>; + + searchText = ''; + + private modelValue: EntityId | null; + private dirty = false; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + 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> { + 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())), + 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); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-notification-dialog.component.html new file mode 100644 index 0000000000..c6eaec492d --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-notification-dialog.component.html @@ -0,0 +1,159 @@ + + +

{{ dialogTitle | translate }}

+ + +
+ + +
+
+ + + check + + + {{ 'notification.basic-settings' | translate }} +
+ + notification.target-name + + + {{ 'notification.target-name-required' | translate }} + + + + + +
+
{{ notificationTemplateTypeTranslateMap.get(notificationType).name | translate }}
+
{{ notificationTemplateTypeTranslateMap.get(notificationType).hint }}
+
+
+
+
+ + notification.subject + + + {{ 'notification.subject-required' | translate }} + + + + notification.message + + + {{ 'notification.message-required' | translate }} + + + +
+
+ + {{ notificationDeliveryMethodTranslateMap.get(deliveryMethods) | translate }} + +
+
+
notification.at-least-one-should-be-selected
+
+
+
+ + {{ 'notification.web-settings' | translate }} +
+ + notification.subject + + + + notification.message + + +
+
+ + {{ 'notification.email-settings' | translate }} + +
+ + notification.subject + + + notification.message + +
+
+
+ + {{ 'notification.sms-settings' | translate }} +
+ + notification.message + + +
+
+ + {{ 'notification.slack-settings' | translate }} +
+ + notification.message + + + + Conversation type + + + + Conversation id + + + {{ 'notification.subject-required' | translate }} + + +
+
+
+
+ +
+ + + + +
diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-notification-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-notification-dialog.component.scss new file mode 100644 index 0000000000..a964169756 --- /dev/null +++ b/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; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-notification-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-notification-dialog.component.ts new file mode 100644 index 0000000000..594ff9d3a0 --- /dev/null +++ b/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 implements OnDestroy { + + @ViewChild('addNotificationTemplate', {static: true}) addNotificationTemplate: MatStepper; + + stepperOrientation: Observable; + + 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; + + private readonly destroy$ = new Subject(); + + constructor(protected store: Store, + protected router: Router, + protected dialogRef: MatDialogRef, + @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}; + }; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/template-dialog/template-table-header.component.html b/ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-table-header.component.html similarity index 100% rename from ui-ngx/src/app/modules/home/pages/notification-center/template-dialog/template-table-header.component.html rename to ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-table-header.component.html diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/template-dialog/template-table-header.component.scss b/ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-table-header.component.scss similarity index 100% rename from ui-ngx/src/app/modules/home/pages/notification-center/template-dialog/template-table-header.component.scss rename to ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-table-header.component.scss diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/template-dialog/template-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-table-header.component.ts similarity index 100% rename from ui-ngx/src/app/modules/home/pages/notification-center/template-dialog/template-table-header.component.ts rename to ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-table-header.component.ts diff --git a/ui-ngx/src/app/shared/components/entity/entity-list.component.html b/ui-ngx/src/app/shared/components/entity/entity-list.component.html index b3fdc9c208..12efddf393 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-list.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-list.component.html @@ -15,8 +15,9 @@ limitations under the License. --> - - + + {{ labelText | translate }} + close - - {{ 'entity.entity-list-empty' | translate }} + {{ requiredText | translate }} diff --git a/ui-ngx/src/app/shared/components/entity/entity-list.component.ts b/ui-ngx/src/app/shared/components/entity/entity-list.component.ts index 5fa43fe053..c1d7d1b157 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-list.component.ts +++ b/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; diff --git a/ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.html b/ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.html index 89be676ce2..9ef4655959 100644 --- a/ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.html @@ -32,7 +32,7 @@ #queueAutocomplete="matAutocomplete" [displayWith]="displayQueueFn" > - + {{getDescription(queue)}} diff --git a/ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.scss b/ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.scss index 45a4ab9808..849cc88ed4 100644 --- a/ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.scss +++ b/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; } diff --git a/ui-ngx/src/app/shared/models/notification.models.ts b/ui-ngx/src/app/shared/models/notification.models.ts index 10ef6102cb..b1f39f9de9 100644 --- a/ui-ngx/src/app/shared/models/notification.models.ts +++ b/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, 'label'> { tenantId?: TenantId; - targets: Array; + targets: Array; templateId: NotificationTemplateId; info?: NotificationInfo; deliveryMethods: Array; @@ -55,6 +56,17 @@ export interface NotificationRequest extends Omit; errors: Map>; @@ -128,12 +140,16 @@ export interface NotificationTemplate extends Omit; + notificationSubject: string; + deliveryMethodsTemplates: { + [key in NotificationDeliveryMethod]: DeliveryMethodNotificationTemplate + }; } -interface DeliveryMethodNotificationTemplate extends +export interface DeliveryMethodNotificationTemplate extends Partial{ body?: string; + enabled: boolean; method: NotificationDeliveryMethod; } @@ -170,13 +186,27 @@ export enum NotificationDeliveryMethod { SLACK = 'SLACK' } +export const NotificationDeliveryMethodTranslateMap = new Map([ + [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.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([ + [NotificationType.GENERAL, + { + name: 'notification.template-type.general', + hint: 'hint' + } + ], + [NotificationType.ALARM, + { + name: 'notification.template-type.alarm', + hint: 'hint' + }] +]); diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 720f74d2a6..8b184cab2f 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/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, diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 441db1594d..cd78329026 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/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", diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index 40278379ad..3fcb0df622 100644 --- a/ui-ngx/yarn.lock +++ b/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"