From 6945b96e3cebb5e847a00d5f05a99e73f222878f Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Mon, 20 Feb 2023 11:30:26 +0200 Subject: [PATCH] UI: Change notification request support send without template --- ui-ngx/src/app/core/ws/websocket.service.ts | 16 ++ ...ack-conversation-autocomplete.component.ts | 12 +- ...request-notification-dialog.component.html | 2 +- ...request-notification-dialog.component.scss | 6 +- .../request-notification-dialog.componet.ts | 14 +- .../template-table/template-configuration.ts | 5 +- .../template-notification-dialog.component.ts | 4 +- ...ashboard-state-autocomplete.component.html | 72 ++++++ .../dashboard-state-autocomplete.component.ts | 229 ++++++++++++++++++ .../app/shared/models/notification.models.ts | 3 +- .../models/websocket/websocket.models.ts | 16 ++ ui-ngx/src/app/shared/shared.module.ts | 3 + 12 files changed, 362 insertions(+), 20 deletions(-) create mode 100644 ui-ngx/src/app/shared/components/dashboard-state-autocomplete.component.html create mode 100644 ui-ngx/src/app/shared/components/dashboard-state-autocomplete.component.ts diff --git a/ui-ngx/src/app/core/ws/websocket.service.ts b/ui-ngx/src/app/core/ws/websocket.service.ts index aeb75f4c4c..51545a6d54 100644 --- a/ui-ngx/src/app/core/ws/websocket.service.ts +++ b/ui-ngx/src/app/core/ws/websocket.service.ts @@ -1,3 +1,19 @@ +/// +/// Copyright © 2016-2023 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 { CmdWrapper, WsService, WsSubscriber } from '@shared/models/websocket/websocket.models'; import { select, Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; diff --git a/ui-ngx/src/app/modules/home/components/notification/slack-conversation-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/notification/slack-conversation-autocomplete.component.ts index 12dbfbf0f8..fcd573ec69 100644 --- a/ui-ngx/src/app/modules/home/components/notification/slack-conversation-autocomplete.component.ts +++ b/ui-ngx/src/app/modules/home/components/notification/slack-conversation-autocomplete.component.ts @@ -16,8 +16,8 @@ import { Component, ElementRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core'; import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; -import { Observable, of } from 'rxjs'; -import { debounceTime, map, publishReplay, refCount, share, switchMap, tap } from 'rxjs/operators'; +import { Observable, of, ReplaySubject } from 'rxjs'; +import { debounceTime, map, share, switchMap, tap } from 'rxjs/operators'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { TranslateService } from '@ngx-translate/core'; @@ -201,8 +201,12 @@ export class SlackConversationAutocompleteComponent implements ControlValueAcces fetchObservable = of([]); } this.slackConversetionFetchObservable$ = fetchObservable.pipe( - publishReplay(1), - refCount() + share({ + connector: () => new ReplaySubject(1), + resetOnError: false, + resetOnComplete: false, + resetOnRefCountZero: false + }) ); } return this.slackConversetionFetchObservable$; diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/request-table/request-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification-center/request-table/request-notification-dialog.component.html index cf27080210..42cd7a3c31 100644 --- a/ui-ngx/src/app/modules/home/pages/notification-center/request-table/request-notification-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification-center/request-table/request-notification-dialog.component.html @@ -36,7 +36,7 @@ {{ 'notification.compose' | translate }} -
+
matches ? 'horizontal' : 'vertical')); this.notificationRequestForm = this.fb.group({ - useTemplate: [true], - templateId: [null, Validators.required], + useTemplate: [false], + templateId: [{value: null, disabled: true}, Validators.required], targets: [null, Validators.required], template: this.templateNotificationForm, additionalConfig: this.fb.group({ @@ -99,9 +99,7 @@ export class RequestNotificationDialogComponent extends }) }); - this.notificationRequestForm.get('template').disable({emitEvent: false}); - this.notificationRequestForm.get('template.name').removeValidators(Validators.required); - this.notificationRequestForm.get('template.name').updateValueAndValidity({emitEvent: false}); + this.notificationRequestForm.get('template.name').setValue(guid()) this.notificationRequestForm.get('useTemplate').valueChanges.pipe( takeUntil(this.destroy$) @@ -203,8 +201,10 @@ export class RequestNotificationDialogComponent extends private get notificationFormValue(): NotificationRequest { const formValue = deepTrim(this.notificationRequestForm.value); + if (!formValue.useTemplate) { + formValue.template = super.getNotificationTemplateValue(); + } delete formValue.useTemplate; - delete formValue.template; let delay = 0; if (formValue.additionalConfig.enabled) { delay = (this.notificationRequestForm.value.additionalConfig.time.valueOf() - this.minDate().valueOf()) / 1000; diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-configuration.ts b/ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-configuration.ts index 3fca05b07e..a023c10722 100644 --- a/ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-configuration.ts +++ b/ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-configuration.ts @@ -30,6 +30,7 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { Router } from '@angular/router'; import { MatDialogRef } from '@angular/material/dialog'; +import { deepClone, deepTrim } from '@core/utils'; @Directive() // tslint:disable-next-line:directive-class-suffix @@ -161,7 +162,7 @@ export abstract class TemplateConfiguration extends DialogComponent< } protected getNotificationTemplateValue(): NotificationTemplate { - const template: NotificationTemplate = this.templateNotificationForm.value; + const template: NotificationTemplate = deepClone(this.templateNotificationForm.value); this.notificationDeliveryMethods.forEach(method => { if (template.configuration.deliveryMethodsTemplates[method].enabled) { Object.assign(template.configuration.deliveryMethodsTemplates[method], this.deliveryMethodFormsMap.get(method).value, {method}); @@ -169,6 +170,6 @@ export abstract class TemplateConfiguration extends DialogComponent< delete template.configuration.deliveryMethodsTemplates[method]; } }); - return template; + return deepTrim(template); } } 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 index 5d3f5d19ee..1aebb16434 100644 --- 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 @@ -27,7 +27,7 @@ import { Router } from '@angular/router'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { FormBuilder, FormGroup } from '@angular/forms'; import { NotificationService } from '@core/http/notification.service'; -import { deepClone, deepTrim, isDefinedAndNotNull } from '@core/utils'; +import { deepClone, isDefinedAndNotNull } from '@core/utils'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { StepperOrientation, StepperSelectionEvent } from '@angular/cdk/stepper'; @@ -155,7 +155,7 @@ export class TemplateNotificationDialogComponent if (this.templateNotification && !this.data.isCopy) { template = {...this.templateNotification, ...template}; } - this.notificationService.saveNotificationTemplate(deepTrim(template)).subscribe( + this.notificationService.saveNotificationTemplate(template).subscribe( (target) => this.dialogRef.close(target) ); } diff --git a/ui-ngx/src/app/shared/components/dashboard-state-autocomplete.component.html b/ui-ngx/src/app/shared/components/dashboard-state-autocomplete.component.html new file mode 100644 index 0000000000..5ce3ff51ca --- /dev/null +++ b/ui-ngx/src/app/shared/components/dashboard-state-autocomplete.component.html @@ -0,0 +1,72 @@ + + + + + + + + + + + {{ 'widget-action.target-dashboard-state-required' | translate }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/app/shared/components/dashboard-state-autocomplete.component.ts b/ui-ngx/src/app/shared/components/dashboard-state-autocomplete.component.ts new file mode 100644 index 0000000000..f6e93d8aea --- /dev/null +++ b/ui-ngx/src/app/shared/components/dashboard-state-autocomplete.component.ts @@ -0,0 +1,229 @@ +/// +/// Copyright © 2016-2023 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, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Observable, of, ReplaySubject } from 'rxjs'; +import { debounceTime, distinctUntilChanged, map, share, switchMap, tap } from 'rxjs/operators'; +import { Dashboard, DashboardInfo } from '@app/shared/models/dashboard.models'; +import { DashboardService } from '@core/http/dashboard.service'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { FloatLabelType } from '@angular/material/form-field'; +import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; + +@Component({ + selector: 'tb-dashboard-state-autocomplete', + templateUrl: './dashboard-state-autocomplete.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DashboardStateAutocompleteComponent), + multi: true + }] +}) +export class DashboardStateAutocompleteComponent implements ControlValueAccessor, OnInit { + + private dirty = false; + private modelValue: string; + + private latestDashboardStates: Array = null; + private dashboardStatesFetchObservable$: Observable> = null; + + private propagateChange = (v: any) => { }; + + + @Input() + placeholder: string; + + @Input() + floatLabel: FloatLabelType = 'auto'; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + private dashboardIdValue: string; + get dashboardId(): string { + return this.dashboardIdValue; + } + + set dashboardId(value: string) { + this.dashboardIdValue = value; + this.clearDashboardStateCache(); + this.searchText = ''; + this.selectDashboardStateFormGroup.get('dashboardStateId').patchValue('', {emitEvent: false}); + this.dirty = true; + } + + @ViewChild('dashboardStateInput', {static: true}) dashboardStateInput: ElementRef; + + filteredStatesDashboard$: Observable>; + + searchText = ''; + + selectDashboardStateFormGroup = this.fb.group({ + dashboardStateId: [null] + }); + + constructor(private store: Store, + public translate: TranslateService, + private dashboardService: DashboardService, + private dashboardUtils: DashboardUtilsService, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + if (this.required) { + this.selectDashboardStateFormGroup.get('dashboardStateId').addValidators(Validators.required); + this.selectDashboardStateFormGroup.get('dashboardStateId').updateValueAndValidity({emitEvent: false}); + } + this.filteredStatesDashboard$ = this.selectDashboardStateFormGroup.get('dashboardStateId').valueChanges + .pipe( + debounceTime(150), + tap(value => { + let modelValue; + if (!value || !this.latestDashboardStates.includes(value)) { + modelValue = null; + } else { + modelValue = value; + } + this.updateView(modelValue); + }), + distinctUntilChanged(), + switchMap(name => this.fetchDashboardStates(name) ), + share() + ); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.selectDashboardStateFormGroup.disable({emitEvent: false}); + } else { + this.selectDashboardStateFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: string | null): void { + this.searchText = ''; + if (value != null) { + this.modelValue = value; + this.selectDashboardStateFormGroup.get('dashboardStateId').patchValue(value, {emitEvent: false}); + } else { + this.modelValue = null; + this.selectDashboardStateFormGroup.get('dashboardStateId').patchValue('', {emitEvent: false}); + } + this.dirty = true; + } + + private updateView(value: string | null) { + if (this.modelValue !== value) { + this.modelValue = value; + this.propagateChange(this.modelValue); + } + } + + displayDashboardFn(dashboard?: DashboardInfo): string | undefined { + return dashboard ? dashboard.title : undefined; + } + + onFocus() { + if (this.dirty) { + this.selectDashboardStateFormGroup.get('dashboard').updateValueAndValidity({onlySelf: true}); + this.dirty = false; + } + } + + clear(value = '') { + this.dashboardStateInput.nativeElement.value = value; + this.selectDashboardStateFormGroup.get('dashboardStateId').patchValue(value, {emitEvent: true}); + setTimeout(() => { + this.dashboardStateInput.nativeElement.blur(); + this.dashboardStateInput.nativeElement.focus(); + }, 0); + } + + private fetchDashboardStates(searchText?: string): Observable> { + if (this.searchText !== searchText || this.latestDashboardStates === null) { + this.searchText = searchText; + const slackConversationFilter = this.createFilterForDashboardState(this.searchText); + return this.getDashboardStatesById().pipe( + map(name => name.filter(slackConversationFilter)), + tap(res => this.latestDashboardStates = res) + ); + } + return of(this.latestDashboardStates); + } + + private getDashboardStatesById() { + if (this.dashboardStatesFetchObservable$ === null) { + let fetchObservable: Observable>; + if (this.dashboardId) { + fetchObservable = this.dashboardService.getDashboard(this.dashboardId, {ignoreLoading: true}).pipe( + map((dashboard: Dashboard) => { + if (dashboard) { + dashboard = this.dashboardUtils.validateAndUpdateDashboard(dashboard); + const states = dashboard.configuration.states; + return Object.keys(states); + } else { + return []; + } + }) + ); + } else { + fetchObservable = of([]); + } + this.dashboardStatesFetchObservable$ = fetchObservable.pipe( + share({ + connector: () => new ReplaySubject(1), + resetOnError: false, + resetOnComplete: false, + resetOnRefCountZero: false + }) + ); + } + return this.dashboardStatesFetchObservable$; + } + + private createFilterForDashboardState(query: string): (stateId: string) => boolean { + const lowercaseQuery = query.toLowerCase(); + return stateId => stateId.toLowerCase().indexOf(lowercaseQuery) === 0; + } + + private clearDashboardStateCache(): void { + this.latestDashboardStates = null; + this.dashboardStatesFetchObservable$ = null; + } + +} diff --git a/ui-ngx/src/app/shared/models/notification.models.ts b/ui-ngx/src/app/shared/models/notification.models.ts index bbee4fae5f..f9f5eec2fd 100644 --- a/ui-ngx/src/app/shared/models/notification.models.ts +++ b/ui-ngx/src/app/shared/models/notification.models.ts @@ -50,7 +50,8 @@ export interface NotificationInfo { export interface NotificationRequest extends Omit, 'label'> { tenantId?: TenantId; targets: Array; - templateId: NotificationTemplateId; + templateId?: NotificationTemplateId; + template?: NotificationTemplate; info?: NotificationInfo; deliveryMethods: Array; originatorEntityId: EntityId; diff --git a/ui-ngx/src/app/shared/models/websocket/websocket.models.ts b/ui-ngx/src/app/shared/models/websocket/websocket.models.ts index 5e5ff77231..fc7fc3d15c 100644 --- a/ui-ngx/src/app/shared/models/websocket/websocket.models.ts +++ b/ui-ngx/src/app/shared/models/websocket/websocket.models.ts @@ -1,3 +1,19 @@ +/// +/// Copyright © 2016-2023 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 { NgZone } from '@angular/core'; import { WebsocketCmd } from '@shared/models/telemetry/telemetry.models'; import { Subject } from 'rxjs'; diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 992cca6a5b..d62f69d67b 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -86,6 +86,7 @@ import { MarkdownEditorComponent } from '@shared/components/markdown-editor.comp import { FullscreenDirective } from '@shared/components/fullscreen.directive'; import { HighlightPipe } from '@shared/pipe/highlight.pipe'; import { DashboardAutocompleteComponent } from '@shared/components/dashboard-autocomplete.component'; +import { DashboardStateAutocompleteComponent } from '@shared/components/dashboard-state-autocomplete.component'; import { EntitySubTypeAutocompleteComponent } from '@shared/components/entity/entity-subtype-autocomplete.component'; import { EntitySubTypeSelectComponent } from '@shared/components/entity/entity-subtype-select.component'; import { EntityAutocompleteComponent } from '@shared/components/entity/entity-autocomplete.component'; @@ -243,6 +244,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) TimezoneSelectComponent, ValueInputComponent, DashboardAutocompleteComponent, + DashboardStateAutocompleteComponent, EntitySubTypeAutocompleteComponent, EntitySubTypeSelectComponent, EntitySubTypeListComponent, @@ -403,6 +405,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) DatetimeComponent, TimezoneSelectComponent, DashboardAutocompleteComponent, + DashboardStateAutocompleteComponent, EntitySubTypeAutocompleteComponent, EntitySubTypeSelectComponent, EntitySubTypeListComponent,