Browse Source

UI: Change notification request support send without template

pull/7911/head
Vladyslav_Prykhodko 4 years ago
parent
commit
6945b96e3c
  1. 16
      ui-ngx/src/app/core/ws/websocket.service.ts
  2. 12
      ui-ngx/src/app/modules/home/components/notification/slack-conversation-autocomplete.component.ts
  3. 2
      ui-ngx/src/app/modules/home/pages/notification-center/request-table/request-notification-dialog.component.html
  4. 6
      ui-ngx/src/app/modules/home/pages/notification-center/request-table/request-notification-dialog.component.scss
  5. 14
      ui-ngx/src/app/modules/home/pages/notification-center/request-table/request-notification-dialog.componet.ts
  6. 5
      ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-configuration.ts
  7. 4
      ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-notification-dialog.component.ts
  8. 72
      ui-ngx/src/app/shared/components/dashboard-state-autocomplete.component.html
  9. 229
      ui-ngx/src/app/shared/components/dashboard-state-autocomplete.component.ts
  10. 3
      ui-ngx/src/app/shared/models/notification.models.ts
  11. 16
      ui-ngx/src/app/shared/models/websocket/websocket.models.ts
  12. 3
      ui-ngx/src/app/shared/shared.module.ts

16
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';

12
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$;

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

@ -36,7 +36,7 @@
</ng-template>
<mat-step [stepControl]="notificationRequestForm">
<ng-template matStepLabel>{{ 'notification.compose' | translate }}</ng-template>
<form [formGroup]="notificationRequestForm" style="padding-bottom: 16px;">
<form [formGroup]="notificationRequestForm">
<div fxLayout="row" fxLayoutAlign="center">
<mat-button-toggle-group class="tb-notification-use-template-toggle-group"
style="width: 320px;"

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

@ -17,7 +17,7 @@
@import "../../../../../../theme";
:host-context(.tb-fullscreen-dialog .mat-dialog-container) {
width: 640px;
width: 800px;
max-height: 100vh;
.tb-form-fields {
@ -169,7 +169,7 @@
}
}
.mat-horizontal-content-container {
height: 540px;
height: 600px;
max-height: 100%;
width: 100%;;
overflow-y: auto;
@ -241,7 +241,7 @@
.mat-form-field {
.mat-form-field-wrapper {
padding-bottom: 0px;
padding-bottom: 0;
.mat-form-field-underline {
position: initial !important;

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

@ -29,7 +29,7 @@ import { Router } from '@angular/router';
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { NotificationService } from '@core/http/notification.service';
import { deepTrim } from '@core/utils';
import { deepTrim, guid } from '@core/utils';
import { Observable } from 'rxjs';
import { EntityType } from '@shared/models/entity-type.models';
import { BreakpointObserver } from '@angular/cdk/layout';
@ -88,8 +88,8 @@ export class RequestNotificationDialogComponent extends
.pipe(map(({matches}) => 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;

5
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<T, R = any> 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<T, R = any> extends DialogComponent<
delete template.configuration.deliveryMethodsTemplates[method];
}
});
return template;
return deepTrim(template);
}
}

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

72
ui-ngx/src/app/shared/components/dashboard-state-autocomplete.component.html

@ -0,0 +1,72 @@
<!--
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.
-->
<mat-form-field [formGroup]="selectDashboardStateFormGroup" class="mat-block" [floatLabel]="floatLabel">
<input matInput type="text" placeholder="{{ placeholder || ('widget-action.target-dashboard-state' | translate) }}"
#dashboardStateInput
formControlName="dashboardStateId"
[required]="required"
[matAutocomplete]="dashboardStateAutocomplete">
<button *ngIf="selectDashboardStateFormGroup.get('dashboardStateId').value"
type="button"
matSuffix mat-icon-button aria-label="Clear"
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>
</button>
<mat-autocomplete
class="tb-autocomplete"
#dashboardStateAutocomplete="matAutocomplete">
<mat-option *ngFor="let state of filteredStatesDashboard$ | async" [value]="state">
<span [innerHTML]="state | highlight:searchText"></span>
</mat-option>
</mat-autocomplete>
<mat-error *ngIf="selectDashboardStateFormGroup.get('dashboardStateId').hasError('required')">
{{ 'widget-action.target-dashboard-state-required' | translate }}
</mat-error>
</mat-form-field>
<!-- <input matInput type="text" placeholder="{{ placeholder || ('dashboard.dashboard' | translate) }}"-->
<!-- #dashboardInput-->
<!-- formControlName="dashboard"-->
<!-- (focusin)="onFocus()"-->
<!-- [required]="required"-->
<!-- [matAutocomplete]="dashboardAutocomplete">-->
<!-- <button *ngIf="selectDashboardStateFormGroup.get('dashboard').value && !disabled"-->
<!-- type="button"-->
<!-- matSuffix mat-button mat-icon-button aria-label="Clear"-->
<!-- (click)="clear()">-->
<!-- <mat-icon class="material-icons">close</mat-icon>-->
<!-- </button>-->
<!-- <mat-autocomplete-->
<!-- class="tb-autocomplete"-->
<!-- #dashboardAutocomplete="matAutocomplete"-->
<!-- [displayWith]="displayDashboardFn">-->
<!-- <mat-option *ngFor="let dashboard of filteredDashboards | async" [value]="dashboard">-->
<!-- <span [innerHTML]="dashboard.title | highlight:searchText"></span>-->
<!-- </mat-option>-->
<!-- <mat-option *ngIf="!(filteredDashboards | async)?.length" [value]="null">-->
<!-- <span>-->
<!-- {{ translate.get('dashboard.no-dashboards-matching', {entity: searchText}) | async }}-->
<!-- </span>-->
<!-- </mat-option>-->
<!-- </mat-autocomplete>-->
<!-- <mat-error>-->
<!-- <ng-content select="[tb-error]"></ng-content>-->
<!-- </mat-error>-->
<!-- <mat-hint>-->
<!-- <ng-content select="[tb-hint]"></ng-content>-->
<!-- </mat-hint>-->
<!--</mat-form-field>-->

229
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<string> = null;
private dashboardStatesFetchObservable$: Observable<Array<string>> = 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<Array<string>>;
searchText = '';
selectDashboardStateFormGroup = this.fb.group({
dashboardStateId: [null]
});
constructor(private store: Store<AppState>,
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<Array<string>> {
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<Array<string>>;
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;
}
}

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

@ -50,7 +50,8 @@ export interface NotificationInfo {
export interface NotificationRequest extends Omit<BaseData<NotificationRequestId>, 'label'> {
tenantId?: TenantId;
targets: Array<string>;
templateId: NotificationTemplateId;
templateId?: NotificationTemplateId;
template?: NotificationTemplate;
info?: NotificationInfo;
deliveryMethods: Array<NotificationDeliveryMethod>;
originatorEntityId: EntityId;

16
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';

3
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,

Loading…
Cancel
Save