From 9fec15121481ff53a386ef91ffe0428c78c2d0ea Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Thu, 12 Jan 2023 18:11:37 +0200 Subject: [PATCH] UI: Add notification template wizard --- ui-ngx/angular.json | 5 + ui-ngx/package.json | 2 + .../template-table-config.ts | 3 +- ...emplate-notification-dialog.component.html | 162 +++++++++++++----- ...emplate-notification-dialog.component.scss | 64 ++++++- .../template-notification-dialog.component.ts | 162 +++++++++++++++++- .../app/shared/models/notification.models.ts | 35 +++- ui-ngx/src/app/shared/shared.module.ts | 6 + .../assets/locale/locale.constant-en_US.json | 17 +- ui-ngx/yarn.lock | 12 ++ 10 files changed, 409 insertions(+), 59 deletions(-) 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/modules/home/pages/notification-center/notification-table/template-table-config.ts b/ui-ngx/src/app/modules/home/pages/notification-center/notification-table/template-table-config.ts index 4615805d37..cee0544763 100644 --- a/ui-ngx/src/app/modules/home/pages/notification-center/notification-table/template-table-config.ts +++ b/ui-ngx/src/app/modules/home/pages/notification-center/notification-table/template-table-config.ts @@ -63,7 +63,8 @@ export class TemplateTableConfig extends EntityTableConfig this.columns.push( new EntityTableColumn('notificationType', 'notification.type', '15%'), new EntityTableColumn('name', 'notification.template', '25%'), - new EntityTableColumn('subject', 'notification.subject', '25%'), + new EntityTableColumn('configuration.notificationSubject', 'notification.subject', '25%', + (template) => template.configuration.notificationSubject, () => ({}), false), new EntityTableColumn('configuration.defaultTextTemplate', 'notification.message', '35%', (template) => template.configuration.defaultTextTemplate, () => ({}), false) ); diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/template-dialog/template-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification-center/template-dialog/template-notification-dialog.component.html index 284ed4cc93..61d8876fb4 100644 --- a/ui-ngx/src/app/modules/home/pages/notification-center/template-dialog/template-notification-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification-center/template-dialog/template-notification-dialog.component.html @@ -27,48 +27,128 @@
-
- - notification.target-name - - - {{ 'notification.target-name-required' | translate }} - - - - - -
-
Generic
-
hint
-
-
- -
Alarm
-
hint
-
-
-
- - notification.message - - - {{ 'notification.message-required' | translate }} - - - - notification.subject - - -
-
+
+ + + 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 }} + + + +
+
+ {{ deliveryMethods }} +
+
+
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 }} + + +
+
+
+
-
- +
+ + + + (click)="nextStep()">{{ nextStepLabel() | 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-dialog/template-notification-dialog.component.scss index 3a3ed8da0b..57e8170cac 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-dialog/template-notification-dialog.component.scss @@ -1,12 +1,72 @@ -:host { - width: 600px; +@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%; + } + } + + @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; 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 index b115544ce5..7119e7dbd4 100644 --- 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 @@ -14,8 +14,14 @@ /// limitations under the License. /// -import { NotificationTemplate, NotificationType } from '@shared/models/notification.models'; -import { Component, Inject, OnDestroy } from '@angular/core'; +import { + DeliveryMethodNotificationTemplate, + NotificationDeliveryMethod, + NotificationTemplate, + NotificationTemplateType, + NotificationTemplateTypeTranslateMap, 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'; @@ -24,7 +30,13 @@ 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'; +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'; +import { NotificationType } from '@core/notification/notification.models'; export interface TemplateNotificationDialogData { template?: NotificationTemplate; @@ -40,11 +52,25 @@ export interface TemplateNotificationDialogData { 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'; - NotificationType = NotificationType; + notificationTypes = Object.keys(NotificationTemplateType) as NotificationType[]; + notificationDeliveryMethods = Object.keys(NotificationDeliveryMethod) as NotificationDeliveryMethod[]; + notificationTemplateTypeTranslateMap = NotificationTemplateTypeTranslateMap; + + selectedIndex = 0; + + tinyMceOptions: Record; private readonly destroy$ = new Subject(); @@ -52,6 +78,7 @@ export class TemplateNotificationDialogComponent 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); @@ -64,13 +91,57 @@ export class TemplateNotificationDialogComponent 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: [null, Validators.required], - type: [NotificationType.GENERIC], + name: ['', Validators.required], + notificationType: [NotificationTemplateType.GENERAL], configuration: this.fb.group({ - defaultTextTemplate: [null, Validators.required] + notificationSubject: ['', Validators.required], + defaultTextTemplate: ['', Validators.required], + deliveryMethodsTemplates: this.fb.group({}) }) }); + 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() { @@ -92,4 +163,81 @@ export class TemplateNotificationDialogComponent (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 >= 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) + ); + } + } + + 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; + } + }); + } } diff --git a/ui-ngx/src/app/shared/models/notification.models.ts b/ui-ngx/src/app/shared/models/notification.models.ts index 10ef6102cb..46955af71d 100644 --- a/ui-ngx/src/app/shared/models/notification.models.ts +++ b/ui-ngx/src/app/shared/models/notification.models.ts @@ -122,18 +122,22 @@ interface CustomerUsersNotificationTargetConfig { export interface NotificationTemplate extends Omit, 'label'>{ tenantId: TenantId; - notificationType: NotificationType; + notificationType: NotificationTemplateType; configuration: NotificationTemplateConfig; } interface NotificationTemplateConfig { defaultTextTemplate: string; - deliveryMethodsTemplates: Map; + notificationSubject: string; + deliveryMethodsTemplates: { + [key in NotificationDeliveryMethod]: DeliveryMethodNotificationTemplate + }; } -interface DeliveryMethodNotificationTemplate extends +export interface DeliveryMethodNotificationTemplate extends Partial{ body?: string; + enabled: boolean; method: NotificationDeliveryMethod; } @@ -176,7 +180,7 @@ export enum NotificationRequestStatus { } export enum SlackChanelType { - USER= 'USER', + DIRECT= 'DIRECT', PUBLIC_CHANNEL = 'PUBLIC_CHANNEL', PRIVATE_CHANNEL = 'PRIVATE_CHANNEL' } @@ -193,7 +197,26 @@ export const NotificationTargetConfigTypeTranslateMap = new Map([ + [NotificationTemplateType.GENERAL, + { + name: 'notification.template-type.general', + hint: 'hint' + } + ], + [NotificationTemplateType.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..5f8065c260 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,15 @@ "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" }, "aggregation": { "aggregation": "Aggregation", @@ -2697,6 +2699,7 @@ "sent": "Sent", "settings": "Notification settings", "subject": "Subject", + "subject-required": "Subject is required", "target-name": "Name", "target-name-required": "Name is required", "target-type": { @@ -2707,9 +2710,19 @@ }, "targets": "Targets", "template": "Template", + "template-type": { + "general": "General", + "alarm": "Alarm" + }, "templates": "Templates", "text": "Text", - "type": "Type" + "type": "Type", + "basic-settings": "Basic settings", + "send-test-notification": "Send test notification", + "web-settings": "Web settings", + "email-settings": "Email settings", + "sms-settings": "SMS settings", + "slack-settings": "Slack settings" }, "ota-update": { "add": "Add package", 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"