65 changed files with 1673 additions and 353 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,20 @@ |
|||
/** |
|||
* Copyright © 2016-2024 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'; |
|||
|
|||
.tb-no-notification-svg-color { |
|||
color: $tb-primary-color; |
|||
} |
|||
@ -0,0 +1,128 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2024 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. |
|||
|
|||
--> |
|||
<ng-container [formGroup]="unreadNotificationWidgetConfigForm"> |
|||
|
|||
<div class="tb-form-panel"> |
|||
<div class="tb-form-panel-title" translate>widget-config.appearance</div> |
|||
<div class="tb-form-row column-xs"> |
|||
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showTitle"> |
|||
{{ 'widget-config.title' | translate }} |
|||
</mat-slide-toggle> |
|||
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
|||
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput formControlName="title" placeholder="{{ 'widget-config.set' | translate }}"> |
|||
</mat-form-field> |
|||
<tb-font-settings formControlName="titleFont" |
|||
clearButton |
|||
[previewText]="unreadNotificationWidgetConfigForm.get('title').value" |
|||
[initialPreviewStyle]="widgetConfig.config.titleStyle"> |
|||
</tb-font-settings> |
|||
<tb-color-input asBoxInput |
|||
colorClearButton |
|||
formControlName="titleColor"> |
|||
</tb-color-input> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row column-xs"> |
|||
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showIcon"> |
|||
{{ 'widgets.notification.icon' | translate }} |
|||
</mat-slide-toggle> |
|||
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
|||
<mat-form-field appearance="outline" class="flex number" subscriptSizing="dynamic"> |
|||
<input matInput type="number" min="0" formControlName="iconSize" placeholder="{{ 'widget-config.set' | translate }}"> |
|||
</mat-form-field> |
|||
<tb-css-unit-select fxFlex formControlName="iconSizeUnit"></tb-css-unit-select> |
|||
<tb-material-icon-select asBoxInput |
|||
iconClearButton |
|||
[color]="unreadNotificationWidgetConfigForm.get('iconColor').value" |
|||
formControlName="icon"> |
|||
</tb-material-icon-select> |
|||
<tb-color-input asBoxInput |
|||
colorClearButton |
|||
formControlName="iconColor"> |
|||
</tb-color-input> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row space-between"> |
|||
<div>{{ 'widgets.notification.max-notification-display' | translate }}</div> |
|||
<mat-form-field appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput type="number" min="1" formControlName="maxNotificationDisplay" placeholder="{{ 'widget-config.set' | translate }}"> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="tb-form-panel"> |
|||
<div class="tb-form-row no-padding no-border"> |
|||
<mat-slide-toggle class="mat-slide" formControlName="showCounter"> |
|||
{{ 'widgets.notification.counter' | translate }} |
|||
</mat-slide-toggle> |
|||
</div> |
|||
<div class="tb-form-row space-between"> |
|||
<div>{{ 'widgets.notification.counter-value' | translate }}</div> |
|||
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
|||
<tb-font-settings formControlName="counterValueFont" |
|||
clearButton |
|||
[previewText]="unreadNotificationWidgetConfigForm.get('maxNotificationDisplay').value.toString()" |
|||
[initialPreviewStyle]="widgetConfig.config.titleStyle"> |
|||
</tb-font-settings> |
|||
<tb-color-input asBoxInput |
|||
formControlName="counterValueColor"> |
|||
</tb-color-input> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row space-between"> |
|||
<div>{{ 'widgets.notification.counter-color' | translate }}</div> |
|||
<tb-color-input asBoxInput |
|||
formControlName="counterColor"> |
|||
</tb-color-input> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="tb-form-panel"> |
|||
<div class="tb-form-panel-title" translate>widget-config.appearance</div> |
|||
<div class="tb-form-row space-between"> |
|||
<div>{{ 'widgets.background.background' | translate }}</div> |
|||
<tb-background-settings formControlName="background"> |
|||
</tb-background-settings> |
|||
</div> |
|||
<div class="tb-form-row space-between column-lt-md"> |
|||
<div translate>widget-config.show-card-buttons</div> |
|||
<mat-chip-listbox multiple formControlName="cardButtons"> |
|||
<mat-chip-option value="viewAll">{{ 'widgets.notification.button-view-all' | translate }}</mat-chip-option> |
|||
<mat-chip-option value="filter">{{ 'widgets.notification.button-filter' | translate }}</mat-chip-option> |
|||
<mat-chip-option value="markAsRead">{{ 'widgets.notification.button-mark-read' | translate }}</mat-chip-option> |
|||
<mat-chip-option value="fullscreen">{{ 'fullscreen.fullscreen' | translate }}</mat-chip-option> |
|||
</mat-chip-listbox> |
|||
</div> |
|||
<div class="tb-form-row space-between"> |
|||
<div>{{ 'widget-config.card-border-radius' | translate }}</div> |
|||
<mat-form-field appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput formControlName="borderRadius" placeholder="{{ 'widget-config.set' | translate }}"> |
|||
</mat-form-field> |
|||
</div> |
|||
<div class="tb-form-row space-between"> |
|||
<div>{{ 'widget-config.card-padding' | translate }}</div> |
|||
<mat-form-field appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput formControlName="padding" placeholder="{{ 'widget-config.set' | translate }}"> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<tb-widget-actions-panel |
|||
formControlName="actions"> |
|||
</tb-widget-actions-panel> |
|||
</ng-container> |
|||
@ -0,0 +1,177 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 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 { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models'; |
|||
import { WidgetConfigComponentData } from '@home/models/widget-component.models'; |
|||
import { WidgetConfig, } from '@shared/models/widget.models'; |
|||
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; |
|||
import { isUndefined } from '@core/utils'; |
|||
import { cssSizeToStrSize, resolveCssSize } from '@shared/models/widget-settings.models'; |
|||
import { |
|||
unreadNotificationDefaultSettings, |
|||
UnreadNotificationWidgetSettings |
|||
} from '@home/components/widget/lib/cards/unread-notification-widget.models'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-unread-notification-basic-config', |
|||
templateUrl: './unread-notification-basic-config.component.html', |
|||
styleUrls: ['../basic-config.scss'] |
|||
}) |
|||
export class UnreadNotificationBasicConfigComponent extends BasicWidgetConfigComponent { |
|||
|
|||
unreadNotificationWidgetConfigForm: UntypedFormGroup; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected widgetConfigComponent: WidgetConfigComponent, |
|||
private fb: UntypedFormBuilder) { |
|||
super(store, widgetConfigComponent); |
|||
} |
|||
|
|||
protected configForm(): UntypedFormGroup { |
|||
return this.unreadNotificationWidgetConfigForm; |
|||
} |
|||
|
|||
protected onConfigSet(configData: WidgetConfigComponentData) { |
|||
const iconSize = resolveCssSize(configData.config.iconSize); |
|||
const settings: UnreadNotificationWidgetSettings = {...unreadNotificationDefaultSettings, ...(configData.config.settings || {})}; |
|||
this.unreadNotificationWidgetConfigForm = this.fb.group({ |
|||
|
|||
showTitle: [configData.config.showTitle, []], |
|||
title: [configData.config.title, []], |
|||
titleFont: [configData.config.titleFont, []], |
|||
titleColor: [configData.config.titleColor, []], |
|||
|
|||
showIcon: [configData.config.showTitleIcon, []], |
|||
iconSize: [iconSize[0], [Validators.min(0)]], |
|||
iconSizeUnit: [iconSize[1], []], |
|||
icon: [configData.config.titleIcon, []], |
|||
iconColor: [configData.config.iconColor, []], |
|||
|
|||
maxNotificationDisplay: [settings.maxNotificationDisplay, [Validators.required, Validators.min(1)]], |
|||
showCounter: [settings.showCounter, []], |
|||
counterValueFont: [settings.counterValueFont, []], |
|||
counterValueColor: [settings.counterValueColor, []], |
|||
counterColor: [settings.counterColor, []], |
|||
|
|||
background: [settings.background, []], |
|||
padding: [settings.padding, []], |
|||
|
|||
cardButtons: [this.getCardButtons(configData.config), []], |
|||
borderRadius: [configData.config.borderRadius, []], |
|||
actions: [configData.config.actions || {}, []] |
|||
}); |
|||
} |
|||
protected validatorTriggers(): string[] { |
|||
return ['showCounter', 'showTitle', 'showIcon']; |
|||
} |
|||
|
|||
protected updateValidators(emitEvent: boolean, trigger?: string) { |
|||
const showCounter: boolean = this.unreadNotificationWidgetConfigForm.get('showCounter').value; |
|||
const showTitle: boolean = this.unreadNotificationWidgetConfigForm.get('showTitle').value; |
|||
const showIcon: boolean = this.unreadNotificationWidgetConfigForm.get('showIcon').value; |
|||
|
|||
if (showTitle) { |
|||
this.unreadNotificationWidgetConfigForm.get('title').enable({emitEvent}); |
|||
this.unreadNotificationWidgetConfigForm.get('titleFont').enable({emitEvent}); |
|||
this.unreadNotificationWidgetConfigForm.get('titleColor').enable({emitEvent}); |
|||
} else { |
|||
this.unreadNotificationWidgetConfigForm.get('title').disable({emitEvent}); |
|||
this.unreadNotificationWidgetConfigForm.get('titleFont').disable({emitEvent}); |
|||
this.unreadNotificationWidgetConfigForm.get('titleColor').disable({emitEvent}); |
|||
} |
|||
|
|||
if (showIcon) { |
|||
this.unreadNotificationWidgetConfigForm.get('iconSize').enable({emitEvent}); |
|||
this.unreadNotificationWidgetConfigForm.get('iconSizeUnit').enable({emitEvent}); |
|||
this.unreadNotificationWidgetConfigForm.get('icon').enable({emitEvent}); |
|||
this.unreadNotificationWidgetConfigForm.get('iconColor').enable({emitEvent}); |
|||
} else { |
|||
this.unreadNotificationWidgetConfigForm.get('iconSize').disable({emitEvent}); |
|||
this.unreadNotificationWidgetConfigForm.get('iconSizeUnit').disable({emitEvent}); |
|||
this.unreadNotificationWidgetConfigForm.get('icon').disable({emitEvent}); |
|||
this.unreadNotificationWidgetConfigForm.get('iconColor').disable({emitEvent}); |
|||
} |
|||
|
|||
if (showCounter) { |
|||
this.unreadNotificationWidgetConfigForm.get('counterValueFont').enable({emitEvent}); |
|||
this.unreadNotificationWidgetConfigForm.get('counterValueColor').enable({emitEvent}); |
|||
this.unreadNotificationWidgetConfigForm.get('counterColor').enable({emitEvent}); |
|||
} else { |
|||
this.unreadNotificationWidgetConfigForm.get('counterValueFont').disable({emitEvent}); |
|||
this.unreadNotificationWidgetConfigForm.get('counterValueColor').disable({emitEvent}); |
|||
this.unreadNotificationWidgetConfigForm.get('counterColor').disable({emitEvent}); |
|||
} |
|||
} |
|||
|
|||
protected prepareOutputConfig(config: any): WidgetConfigComponentData { |
|||
|
|||
this.widgetConfig.config.showTitle = config.showTitle; |
|||
this.widgetConfig.config.title = config.title; |
|||
this.widgetConfig.config.titleFont = config.titleFont; |
|||
this.widgetConfig.config.titleColor = config.titleColor; |
|||
|
|||
this.widgetConfig.config.showTitleIcon = config.showIcon; |
|||
this.widgetConfig.config.iconSize = cssSizeToStrSize(config.iconSize, config.iconSizeUnit); |
|||
this.widgetConfig.config.titleIcon = config.icon; |
|||
this.widgetConfig.config.iconColor = config.iconColor; |
|||
|
|||
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; |
|||
|
|||
this.widgetConfig.config.settings.maxNotificationDisplay = config.maxNotificationDisplay; |
|||
this.widgetConfig.config.settings.showCounter = config.showCounter; |
|||
this.widgetConfig.config.settings.counterValueFont = config.counterValueFont; |
|||
this.widgetConfig.config.settings.counterValueColor = config.counterValueColor; |
|||
this.widgetConfig.config.settings.counterColor = config.counterColor; |
|||
|
|||
this.widgetConfig.config.settings.background = config.background; |
|||
this.widgetConfig.config.settings.padding = config.padding; |
|||
|
|||
this.widgetConfig.config.actions = config.actions; |
|||
this.setCardButtons(config.cardButtons, this.widgetConfig.config); |
|||
this.widgetConfig.config.borderRadius = config.borderRadius; |
|||
return this.widgetConfig; |
|||
} |
|||
|
|||
private getCardButtons(config: WidgetConfig): string[] { |
|||
const buttons: string[] = []; |
|||
if (isUndefined(config.settings?.enableViewAll) || config.settings?.enableViewAll) { |
|||
buttons.push('viewAll'); |
|||
} |
|||
if (isUndefined(config.settings?.enableFilter) || config.settings?.enableFilter) { |
|||
buttons.push('filter'); |
|||
} |
|||
if (isUndefined(config.settings?.enableMarkAsRead) || config.settings?.enableMarkAsRead) { |
|||
buttons.push('markAsRead'); |
|||
} |
|||
if (isUndefined(config.enableFullscreen) || config.enableFullscreen) { |
|||
buttons.push('fullscreen'); |
|||
} |
|||
return buttons; |
|||
} |
|||
|
|||
private setCardButtons(buttons: string[], config: WidgetConfig) { |
|||
config.settings.enableViewAll = buttons.includes('viewAll'); |
|||
config.settings.enableFilter = buttons.includes('filter'); |
|||
config.settings.enableMarkAsRead = buttons.includes('markAsRead'); |
|||
|
|||
config.enableFullscreen = buttons.includes('fullscreen'); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,77 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2024 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. |
|||
|
|||
--> |
|||
<form fxLayout="column" fxFlex class="mat-content mat-padding" (ngSubmit)="update()"> |
|||
<div class="tb-form-panel no-padding"> |
|||
<div class="tb-form-row column-xs"> |
|||
<div class="fixed-title-width" ngClass.xs="filters-title-mobile" translate>widgets.notification.notification-types</div> |
|||
<mat-form-field floatLabel="auto" appearance="outline" subscriptSizing="dynamic" class="flex fb-chips"> |
|||
<mat-chip-grid #chipList> |
|||
<mat-chip-row |
|||
*ngFor="let type of selectedNotificationTypes" |
|||
removable |
|||
(removed)="remove(type)"> |
|||
{{ notificationTypesTranslateMap.get(type).name | translate }} |
|||
<mat-icon matChipRemove>close</mat-icon> |
|||
</mat-chip-row> |
|||
<input matInput type="text" |
|||
placeholder="{{ ( selectedNotificationTypes.length ? 'widgets.notification.notification-type' : 'widgets.notification.any-type' ) | translate }}" |
|||
style="max-width: 200px;" |
|||
[formControl]="searchControlName" |
|||
#notificationTypeInput |
|||
(focusin)="onFocus()" |
|||
matAutocompleteOrigin |
|||
#origin="matAutocompleteOrigin" |
|||
[matAutocompleteConnectedTo]="origin" |
|||
[matAutocomplete]="NotificationTypeAutocomplete" |
|||
[matChipInputFor]="chipList" |
|||
[matChipInputSeparatorKeyCodes]="separatorKeysCodes" |
|||
(matChipInputTokenEnd)="chipAdd($event)"> |
|||
</mat-chip-grid> |
|||
<mat-autocomplete #NotificationTypeAutocomplete="matAutocomplete" |
|||
class="tb-autocomplete" |
|||
(optionSelected)="selected($event)" |
|||
[displayWith]="displayTypeFn"> |
|||
<mat-option *ngFor="let type of filteredNotificationTypesList | async" [value]="type"> |
|||
<span [innerHTML]="notificationTypesTranslateMap.get(type).name | translate | highlight:searchText"></span> |
|||
</mat-option> |
|||
</mat-autocomplete> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
|
|||
<div fxLayout="row" class="tb-panel-actions" fxLayoutAlign="end center"> |
|||
<button type="button" |
|||
mat-button |
|||
(click)="reset()" |
|||
color="primary"> |
|||
{{ 'action.reset' | translate }} |
|||
</button> |
|||
<span fxFlex></span> |
|||
<button type="button" |
|||
(click)="cancel()" |
|||
mat-button> |
|||
{{ 'action.cancel' | translate }} |
|||
</button> |
|||
<button type="submit" |
|||
mat-raised-button |
|||
color="primary"> |
|||
{{ 'action.update' | translate }} |
|||
</button> |
|||
</div> |
|||
</form> |
|||
|
|||
@ -0,0 +1,25 @@ |
|||
/** |
|||
* Copyright © 2016-2024 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. |
|||
*/ |
|||
|
|||
:host { |
|||
display: flex; |
|||
width: 100%; |
|||
max-width: 100%; |
|||
|
|||
.mdc-button { |
|||
max-width: 100%; |
|||
} |
|||
} |
|||
@ -0,0 +1,140 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 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, Inject, InjectionToken, OnInit, ViewChild } from '@angular/core'; |
|||
import { NotificationTemplateTypeTranslateMap, NotificationType } from '@shared/models/notification.models'; |
|||
import { MatChipInputEvent } from '@angular/material/chips'; |
|||
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; |
|||
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; |
|||
import { Observable } from 'rxjs'; |
|||
import { FormControl } from '@angular/forms'; |
|||
import { debounceTime, map } from 'rxjs/operators'; |
|||
import { OverlayRef } from '@angular/cdk/overlay'; |
|||
|
|||
export const NOTIFICATION_TYPE_FILTER_PANEL_DATA = new InjectionToken<any>('NotificationTypeFilterPanelData'); |
|||
|
|||
export interface NotificationTypeFilterPanelData { |
|||
notificationTypes: Array<NotificationType>; |
|||
notificationTypesUpdated: (notificationTypes: Array<NotificationType>) => void; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-notification-type-filter-panel', |
|||
templateUrl: './notification-type-filter-panel.component.html', |
|||
styleUrls: ['notification-type-filter-panel.component.scss'] |
|||
}) |
|||
export class NotificationTypeFilterPanelComponent implements OnInit{ |
|||
|
|||
@ViewChild('searchInput') searchInputField: ElementRef; |
|||
|
|||
searchText = ''; |
|||
searchControlName = new FormControl(''); |
|||
|
|||
filteredNotificationTypesList: Observable<Array<NotificationType>>; |
|||
selectedNotificationTypes: Array<NotificationType> = []; |
|||
notificationTypesTranslateMap = NotificationTemplateTypeTranslateMap; |
|||
|
|||
separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON]; |
|||
|
|||
private notificationType = NotificationType; |
|||
private notificationTypes = Object.keys(NotificationType) as Array<NotificationType>; |
|||
|
|||
private dirty = false; |
|||
|
|||
@ViewChild('notificationTypeInput') notificationTypeInput: ElementRef<HTMLInputElement>; |
|||
|
|||
constructor(@Inject(NOTIFICATION_TYPE_FILTER_PANEL_DATA) public data: NotificationTypeFilterPanelData, |
|||
private overlayRef: OverlayRef) { |
|||
this.selectedNotificationTypes = this.data.notificationTypes; |
|||
this.dirty = true; |
|||
} |
|||
|
|||
ngOnInit() { |
|||
this.filteredNotificationTypesList = this.searchControlName.valueChanges.pipe( |
|||
debounceTime(150), |
|||
map(value => { |
|||
this.searchText = value; |
|||
return this.notificationTypes.filter(type => !this.selectedNotificationTypes.includes(type)) |
|||
.filter(type => value ? type.toUpperCase().startsWith(value.toUpperCase()) : true); |
|||
}) |
|||
); |
|||
} |
|||
|
|||
public update() { |
|||
this.data.notificationTypesUpdated(this.selectedNotificationTypes); |
|||
if (this.overlayRef) { |
|||
this.overlayRef.dispose(); |
|||
} |
|||
} |
|||
|
|||
cancel() { |
|||
if (this.overlayRef) { |
|||
this.overlayRef.dispose(); |
|||
} |
|||
} |
|||
|
|||
public reset() { |
|||
this.selectedNotificationTypes.length = 0; |
|||
this.searchControlName.updateValueAndValidity({emitEvent: true}); |
|||
} |
|||
|
|||
remove(type: NotificationType) { |
|||
const index = this.selectedNotificationTypes.indexOf(type); |
|||
if (index >= 0) { |
|||
this.selectedNotificationTypes.splice(index, 1); |
|||
this.searchControlName.updateValueAndValidity({emitEvent: true}); |
|||
} |
|||
} |
|||
|
|||
onFocus() { |
|||
if (this.dirty) { |
|||
this.searchControlName.updateValueAndValidity({emitEvent: true}); |
|||
this.dirty = false; |
|||
} |
|||
} |
|||
|
|||
private add(type: NotificationType): void { |
|||
this.selectedNotificationTypes.push(type); |
|||
} |
|||
|
|||
chipAdd(event: MatChipInputEvent): void { |
|||
const value = (event.value || '').trim(); |
|||
if (value && this.notificationType[value]) { |
|||
this.add(this.notificationType[value]); |
|||
this.clear(''); |
|||
} |
|||
} |
|||
|
|||
selected(event: MatAutocompleteSelectedEvent): void { |
|||
if (this.notificationType[event.option.value]) { |
|||
this.add(this.notificationType[event.option.value]); |
|||
} |
|||
this.clear(''); |
|||
} |
|||
|
|||
clear(value: string = '') { |
|||
this.notificationTypeInput.nativeElement.value = value; |
|||
this.searchControlName.patchValue(value, {emitEvent: true}); |
|||
setTimeout(() => { |
|||
this.notificationTypeInput.nativeElement.blur(); |
|||
this.notificationTypeInput.nativeElement.focus(); |
|||
}, 0); |
|||
} |
|||
|
|||
displayTypeFn(type?: string): string | undefined { |
|||
return type ? type : undefined; |
|||
} |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2024 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<div class="tb-unread-notification-panel" [style.padding]="padding" [style]="backgroundStyle$ | async"> |
|||
<div class="tb-unread-notification-overlay" [style]="overlayStyle"></div> |
|||
<ng-template #counter> |
|||
<div *ngIf="showCounter" class="notification-counter" [style.background-color]="counterBackground"> |
|||
<span class="notification-counter-value" [style]="counterValueStyle">{{ count$ | async }}</span> |
|||
</div> |
|||
</ng-template> |
|||
<ng-container *ngTemplateOutlet="widgetTitlePanel; context:{ titleSuffixTemplate: counter }"></ng-container> |
|||
|
|||
<div class="tb-unread-notification-content"> |
|||
<ng-container *ngIf="loadNotification; else loadingNotification"> |
|||
<div *ngIf="notifications.length; else emptyNotification" style="overflow: auto; width: 100%;"> |
|||
<section style="min-height: 100px; overflow: auto; padding: 6px 0;"> |
|||
<div *ngFor="let notification of notifications; let last = last; trackBy: trackById"> |
|||
<tb-notification [notification]="notification" |
|||
(markAsRead)="markAsRead($event)"> |
|||
</tb-notification> |
|||
</div> |
|||
</section> |
|||
</div> |
|||
</ng-container> |
|||
<ng-template #emptyNotification> |
|||
<div class="tb-no-notification-svg-color" style="height: 85%;"> |
|||
<svg height="100%" preserveAspectRatio="xMidYMid meet" viewBox="0 0 149 156" width="100%"> |
|||
<use [attr.xlink:href]="'assets/notification-bell.svg#CHECK_ICON'"></use> |
|||
</svg> |
|||
</div> |
|||
<span class="tb-no-notification-text" translate>notification.no-notifications-yet</span> |
|||
</ng-template> |
|||
<ng-template #loadingNotification> |
|||
<div class="tb-no-data-available" style="margin: 20px; gap: 16px;"> |
|||
<mat-spinner color="accent" diameter="65" strokeWidth="4"></mat-spinner> |
|||
<div class="tb-no-data-text" translate>notification.loading-notifications</div> |
|||
</div> |
|||
</ng-template> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,65 @@ |
|||
/** |
|||
* Copyright © 2016-2024 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"; |
|||
|
|||
.tb-no-notification-svg-color { |
|||
color: $tb-primary-color; |
|||
} |
|||
|
|||
.tb-unread-notification-panel { |
|||
width: 100%; |
|||
height: 100%; |
|||
position: relative; |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 8px; |
|||
padding: 20px 24px 24px 24px; |
|||
> div:not(.tb-unread-notification-overlay) { |
|||
z-index: 1; |
|||
} |
|||
div.tb-widget-title { |
|||
padding: 0; |
|||
} |
|||
.tb-unread-notification-overlay { |
|||
position: absolute; |
|||
top: 12px; |
|||
left: 12px; |
|||
bottom: 12px; |
|||
right: 12px; |
|||
} |
|||
.tb-unread-notification-content { |
|||
height: 100%; |
|||
min-height: 0; |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
.tb-no-notification-text { |
|||
text-align: center; |
|||
margin-bottom: 12px; |
|||
color: rgba(0, 0, 0, 0.38); |
|||
} |
|||
} |
|||
.notification-counter { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
width: 24px; |
|||
height: 22px; |
|||
background-color: green; |
|||
border-radius: 7px; |
|||
} |
|||
} |
|||
@ -0,0 +1,284 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 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 { |
|||
ChangeDetectorRef, |
|||
Component, |
|||
Injector, |
|||
Input, |
|||
NgZone, |
|||
OnDestroy, |
|||
OnInit, |
|||
StaticProvider, |
|||
TemplateRef, |
|||
ViewContainerRef, |
|||
ViewEncapsulation |
|||
} from '@angular/core'; |
|||
import { WidgetAction, WidgetContext } from '@home/models/widget-component.models'; |
|||
import { isDefined } from '@core/utils'; |
|||
import { backgroundStyle, ComponentStyle, overlayStyle, textStyle } from '@shared/models/widget-settings.models'; |
|||
import { ResizeObserver } from '@juggle/resize-observer'; |
|||
import { BehaviorSubject, fromEvent, Observable, ReplaySubject, Subscription } from 'rxjs'; |
|||
import { ImagePipe } from '@shared/pipe/image.pipe'; |
|||
import { DomSanitizer } from '@angular/platform-browser'; |
|||
import { |
|||
unreadNotificationDefaultSettings, |
|||
UnreadNotificationWidgetSettings |
|||
} from '@home/components/widget/lib/cards/unread-notification-widget.models'; |
|||
import { Notification, NotificationRequest, NotificationType } from '@shared/models/notification.models'; |
|||
import { NotificationSubscriber } from '@shared/models/telemetry/telemetry.models'; |
|||
import { NotificationWebsocketService } from '@core/ws/notification-websocket.service'; |
|||
import { distinctUntilChanged, map, share, skip, take, tap } from 'rxjs/operators'; |
|||
import { Router } from '@angular/router'; |
|||
import { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; |
|||
import { DEFAULT_OVERLAY_POSITIONS } from '@shared/models/overlay.models'; |
|||
import { ComponentPortal } from '@angular/cdk/portal'; |
|||
import { |
|||
NOTIFICATION_TYPE_FILTER_PANEL_DATA, |
|||
NotificationTypeFilterPanelComponent |
|||
} from '@home/components/widget/lib/cards/notification-type-filter-panel.component'; |
|||
import { selectUserDetails } from '@core/auth/auth.selectors'; |
|||
import { select, Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-unread-notification-widget', |
|||
templateUrl: './unread-notification-widget.component.html', |
|||
styleUrls: ['unread-notification-widget.component.scss'], |
|||
encapsulation: ViewEncapsulation.None |
|||
}) |
|||
export class UnreadNotificationWidgetComponent implements OnInit, OnDestroy { |
|||
|
|||
settings: UnreadNotificationWidgetSettings; |
|||
|
|||
@Input() |
|||
ctx: WidgetContext; |
|||
|
|||
@Input() |
|||
widgetTitlePanel: TemplateRef<any>; |
|||
|
|||
showCounter = true; |
|||
counterValueStyle: ComponentStyle; |
|||
counterBackground: string; |
|||
|
|||
notifications: Notification[]; |
|||
loadNotification = false; |
|||
|
|||
backgroundStyle$: Observable<ComponentStyle>; |
|||
overlayStyle: ComponentStyle = {}; |
|||
padding: string; |
|||
|
|||
private counterValue: BehaviorSubject<number> = new BehaviorSubject(0); |
|||
|
|||
count$ = this.counterValue.asObservable().pipe( |
|||
distinctUntilChanged(), |
|||
map((value) => value >= 100 ? '99+' : value), |
|||
tap(() => Promise.resolve().then(() => this.cd.markForCheck())), |
|||
share({ |
|||
connector: () => new ReplaySubject(1) |
|||
}) |
|||
); |
|||
|
|||
|
|||
private notificationTypes: Array<NotificationType> = []; |
|||
|
|||
private notificationSubscriber: NotificationSubscriber; |
|||
private notificationCountSubscriber: Subscription; |
|||
private notification: Subscription; |
|||
|
|||
private contentResize$: ResizeObserver; |
|||
|
|||
private defaultDashboardFullscreen = false; |
|||
|
|||
private viewAllAction: WidgetAction = { |
|||
name: 'widgets.notification.button-view-all', |
|||
show: !this.defaultDashboardFullscreen, |
|||
icon: 'open_in_new', |
|||
onAction: ($event) => { |
|||
this.viewAll($event); |
|||
} |
|||
}; |
|||
|
|||
private filterAction: WidgetAction = { |
|||
name: 'widgets.notification.button-filter', |
|||
show: true, |
|||
icon: 'filter_list', |
|||
onAction: ($event) => { |
|||
this.editNotificationTypeFilter($event); |
|||
} |
|||
}; |
|||
|
|||
private markAsReadAction: WidgetAction = { |
|||
name: 'widgets.notification.button-mark-read', |
|||
show: true, |
|||
icon: 'done_all', |
|||
onAction: ($event) => { |
|||
this.markAsAllRead($event); |
|||
} |
|||
}; |
|||
|
|||
constructor(private store: Store<AppState>, |
|||
private imagePipe: ImagePipe, |
|||
private notificationWsService: NotificationWebsocketService, |
|||
private sanitizer: DomSanitizer, |
|||
private router: Router, |
|||
private zone: NgZone, |
|||
private overlay: Overlay, |
|||
private viewContainerRef: ViewContainerRef, |
|||
private cd: ChangeDetectorRef) { |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.ctx.$scope.unreadNotificationWidget = this; |
|||
this.settings = {...unreadNotificationDefaultSettings, ...this.ctx.settings}; |
|||
|
|||
this.showCounter = this.settings.showCounter; |
|||
this.counterValueStyle = textStyle(this.settings.counterValueFont); |
|||
this.counterValueStyle.color = this.settings.counterValueColor; |
|||
this.counterBackground = this.settings.counterColor; |
|||
|
|||
this.ctx.widgetActions = [this.viewAllAction, this.filterAction, this.markAsReadAction]; |
|||
|
|||
this.viewAllAction.show = isDefined(this.settings.enableViewAll) ? this.settings.enableViewAll : true; |
|||
this.store.pipe(select(selectUserDetails), take(1)).subscribe( |
|||
user => this.viewAllAction.show = !user.additionalInfo?.defaultDashboardFullscreen |
|||
); |
|||
this.filterAction.show = isDefined(this.settings.enableFilter) ? this.settings.enableFilter : true; |
|||
this.markAsReadAction.show = isDefined(this.settings.enableMarkAsRead) ? this.settings.enableMarkAsRead : true; |
|||
|
|||
this.initSubscription(); |
|||
|
|||
this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer); |
|||
this.overlayStyle = overlayStyle(this.settings.background.overlay); |
|||
this.padding = this.settings.background.overlay.enabled ? undefined : this.settings.padding; |
|||
} |
|||
|
|||
|
|||
ngOnDestroy() { |
|||
if (this.contentResize$) { |
|||
this.contentResize$.disconnect(); |
|||
} |
|||
this.unsubscribeSubscription(); |
|||
} |
|||
|
|||
private initSubscription() { |
|||
this.notificationSubscriber = NotificationSubscriber.createNotificationsSubscription( |
|||
this.notificationWsService, this.zone, this.settings.maxNotificationDisplay, this.notificationTypes); |
|||
this.notification = this.notificationSubscriber.notifications$.subscribe(value => { |
|||
if (Array.isArray(value)) { |
|||
this.loadNotification = true; |
|||
this.notifications = value; |
|||
this.cd.markForCheck(); |
|||
} |
|||
}); |
|||
this.notificationCountSubscriber = this.notificationSubscriber.notificationCount$.pipe( |
|||
skip(1), |
|||
).subscribe(value => this.counterValue.next(value)); |
|||
this.notificationSubscriber.subscribe(); |
|||
} |
|||
|
|||
private unsubscribeSubscription() { |
|||
this.notificationSubscriber.unsubscribe(); |
|||
this.notificationCountSubscriber.unsubscribe(); |
|||
this.notification.unsubscribe(); |
|||
} |
|||
|
|||
public onInit() { |
|||
const borderRadius = this.ctx.$widgetElement.css('borderRadius'); |
|||
this.overlayStyle = {...this.overlayStyle, ...{borderRadius}}; |
|||
this.cd.detectChanges(); |
|||
} |
|||
|
|||
markAsRead(id: string) { |
|||
const cmd = NotificationSubscriber.createMarkAsReadCommand(this.notificationWsService, [id]); |
|||
cmd.subscribe(); |
|||
} |
|||
|
|||
markAsAllRead($event: Event) { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
const cmd = NotificationSubscriber.createMarkAllAsReadCommand(this.notificationWsService); |
|||
cmd.subscribe(); |
|||
} |
|||
|
|||
viewAll($event: Event) { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
this.router.navigateByUrl(this.router.parseUrl('/notification/inbox')).then(() => {}); |
|||
} |
|||
|
|||
trackById(index: number, item: NotificationRequest): string { |
|||
return item.id.id; |
|||
} |
|||
|
|||
private editNotificationTypeFilter($event: Event) { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
const target = $event.target || $event.srcElement || $event.currentTarget; |
|||
const config = new OverlayConfig({ |
|||
panelClass: 'tb-panel-container', |
|||
backdropClass: 'cdk-overlay-transparent-backdrop', |
|||
hasBackdrop: true, |
|||
height: 'fit-content', |
|||
maxHeight: '75vh', |
|||
width: '100%', |
|||
maxWidth: 700 |
|||
}); |
|||
config.positionStrategy = this.overlay.position() |
|||
.flexibleConnectedTo(target as HTMLElement) |
|||
.withPositions(DEFAULT_OVERLAY_POSITIONS); |
|||
|
|||
const overlayRef = this.overlay.create(config); |
|||
overlayRef.backdropClick().subscribe(() => { |
|||
overlayRef.dispose(); |
|||
}); |
|||
|
|||
const providers: StaticProvider[] = [ |
|||
{ |
|||
provide: NOTIFICATION_TYPE_FILTER_PANEL_DATA, |
|||
useValue: { |
|||
notificationTypes: this.notificationTypes, |
|||
notificationTypesUpdated: (notificationTypes: Array<NotificationType>) => { |
|||
this.notificationTypes = notificationTypes; |
|||
this.unsubscribeSubscription(); |
|||
this.initSubscription(); |
|||
} |
|||
} |
|||
}, |
|||
{ |
|||
provide: OverlayRef, |
|||
useValue: overlayRef |
|||
} |
|||
]; |
|||
|
|||
const injector = Injector.create({parent: this.viewContainerRef.injector, providers}); |
|||
const componentRef = overlayRef.attach(new ComponentPortal(NotificationTypeFilterPanelComponent, |
|||
this.viewContainerRef, injector)); |
|||
|
|||
const resizeWindows$ = fromEvent(window, 'resize').subscribe(() => { |
|||
overlayRef.updatePosition(); |
|||
}); |
|||
componentRef.onDestroy(() => { |
|||
resizeWindows$.unsubscribe(); |
|||
}); |
|||
|
|||
this.ctx.detectChanges(); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,59 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 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 { BackgroundSettings, BackgroundType, Font } from '@shared/models/widget-settings.models'; |
|||
|
|||
export interface UnreadNotificationWidgetSettings { |
|||
maxNotificationDisplay: number; |
|||
showCounter: boolean; |
|||
counterValueFont: Font; |
|||
counterValueColor: string; |
|||
counterColor: string; |
|||
|
|||
enableViewAll: boolean; |
|||
enableFilter: boolean; |
|||
enableMarkAsRead: boolean; |
|||
background: BackgroundSettings; |
|||
padding: string; |
|||
} |
|||
|
|||
export const unreadNotificationDefaultSettings: UnreadNotificationWidgetSettings = { |
|||
maxNotificationDisplay: 6, |
|||
showCounter: true, |
|||
counterValueFont: { |
|||
family: 'Roboto', |
|||
size: 14, |
|||
sizeUnit: 'px', |
|||
style: 'normal', |
|||
weight: '600', |
|||
lineHeight: '' |
|||
}, |
|||
counterValueColor: '#fff', |
|||
counterColor: '#305680', |
|||
enableViewAll: true, |
|||
enableFilter: true, |
|||
enableMarkAsRead: true, |
|||
background: { |
|||
type: BackgroundType.color, |
|||
color: '#fff', |
|||
overlay: { |
|||
enabled: false, |
|||
color: 'rgba(255,255,255,0.72)', |
|||
blur: 3 |
|||
} |
|||
}, |
|||
padding: '12px' |
|||
}; |
|||
@ -0,0 +1,263 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 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 { isFunction } from '@core/utils'; |
|||
import { FormattedData } from '@shared/models/widget.models'; |
|||
import { DateFormatProcessor, DateFormatSettings, Font } from '@shared/models/widget-settings.models'; |
|||
import { |
|||
TimeSeriesChartDataItem, |
|||
} from '@home/components/widget/lib/chart/time-series-chart.models'; |
|||
import { Renderer2, SecurityContext } from '@angular/core'; |
|||
import { DomSanitizer } from '@angular/platform-browser'; |
|||
import { CallbackDataParams } from 'echarts/types/dist/shared'; |
|||
import { Interval } from '@shared/models/time/time.models'; |
|||
|
|||
export type TimeSeriesChartTooltipValueFormatFunction = |
|||
(value: any, latestData: FormattedData, units?: string, decimals?: number) => string; |
|||
|
|||
export interface TimeSeriesChartTooltipWidgetSettings { |
|||
showTooltip: boolean; |
|||
tooltipTrigger?: TimeSeriesChartTooltipTrigger; |
|||
tooltipShowFocusedSeries?: boolean; |
|||
tooltipLabelFont: Font; |
|||
tooltipLabelColor: string; |
|||
tooltipValueFont: Font; |
|||
tooltipValueColor: string; |
|||
tooltipValueFormatter?: string | TimeSeriesChartTooltipValueFormatFunction; |
|||
tooltipShowDate: boolean; |
|||
tooltipDateInterval?: boolean; |
|||
tooltipDateFormat: DateFormatSettings; |
|||
tooltipDateFont: Font; |
|||
tooltipDateColor: string; |
|||
tooltipBackgroundColor: string; |
|||
tooltipBackgroundBlur: number; |
|||
} |
|||
|
|||
export enum TimeSeriesChartTooltipTrigger { |
|||
point = 'point', |
|||
axis = 'axis' |
|||
} |
|||
|
|||
export const tooltipTriggerTranslationMap = new Map<TimeSeriesChartTooltipTrigger, string>( |
|||
[ |
|||
[TimeSeriesChartTooltipTrigger.point, 'tooltip.trigger-point'], |
|||
[TimeSeriesChartTooltipTrigger.axis, 'tooltip.trigger-axis'] |
|||
] |
|||
); |
|||
|
|||
interface TooltipItem { |
|||
param: CallbackDataParams; |
|||
dataItem: TimeSeriesChartDataItem; |
|||
} |
|||
|
|||
interface TooltipParams { |
|||
items: TooltipItem[]; |
|||
comparisonItems: TooltipItem[]; |
|||
} |
|||
|
|||
export const createTooltipValueFormatFunction = |
|||
(tooltipValueFormatter: string | TimeSeriesChartTooltipValueFormatFunction): TimeSeriesChartTooltipValueFormatFunction => { |
|||
let tooltipValueFormatFunction: TimeSeriesChartTooltipValueFormatFunction; |
|||
if (isFunction(tooltipValueFormatter)) { |
|||
tooltipValueFormatFunction = tooltipValueFormatter as TimeSeriesChartTooltipValueFormatFunction; |
|||
} else if (typeof tooltipValueFormatter === 'string' && tooltipValueFormatter.length) { |
|||
try { |
|||
tooltipValueFormatFunction = |
|||
new Function('value', 'latestData', tooltipValueFormatter) as TimeSeriesChartTooltipValueFormatFunction; |
|||
} catch (e) { |
|||
} |
|||
} |
|||
return tooltipValueFormatFunction; |
|||
}; |
|||
|
|||
export class TimeSeriesChartTooltip { |
|||
|
|||
constructor(private renderer: Renderer2, |
|||
private sanitizer: DomSanitizer, |
|||
private settings: TimeSeriesChartTooltipWidgetSettings, |
|||
private tooltipDateFormat: DateFormatProcessor, |
|||
private valueFormatFunction: TimeSeriesChartTooltipValueFormatFunction) { |
|||
|
|||
} |
|||
|
|||
formatted(params: CallbackDataParams[] | CallbackDataParams, focusedSeriesIndex: number, |
|||
series?: TimeSeriesChartDataItem[], interval?: Interval): HTMLElement { |
|||
if (!this.settings.showTooltip) { |
|||
return undefined; |
|||
} |
|||
|
|||
const tooltipParams = TimeSeriesChartTooltip.mapTooltipParams(params, series, focusedSeriesIndex); |
|||
if (!tooltipParams.items.length && !tooltipParams.comparisonItems.length) { |
|||
return null; |
|||
} |
|||
|
|||
const tooltipElement: HTMLElement = this.renderer.createElement('div'); |
|||
this.renderer.setStyle(tooltipElement, 'display', 'flex'); |
|||
this.renderer.setStyle(tooltipElement, 'flex-direction', 'column'); |
|||
this.renderer.setStyle(tooltipElement, 'align-items', 'flex-start'); |
|||
this.renderer.setStyle(tooltipElement, 'gap', '16px'); |
|||
|
|||
this.buildItemsTooltip(tooltipElement, tooltipParams.items, interval); |
|||
this.buildItemsTooltip(tooltipElement, tooltipParams.comparisonItems, interval); |
|||
|
|||
return tooltipElement; |
|||
} |
|||
|
|||
private buildItemsTooltip(tooltipElement: HTMLElement, |
|||
items: TooltipItem[], interval?: Interval) { |
|||
if (items.length) { |
|||
const tooltipItemsElement: HTMLElement = this.renderer.createElement('div'); |
|||
this.renderer.setStyle(tooltipItemsElement, 'display', 'flex'); |
|||
this.renderer.setStyle(tooltipItemsElement, 'flex-direction', 'column'); |
|||
this.renderer.setStyle(tooltipItemsElement, 'align-items', 'flex-start'); |
|||
this.renderer.setStyle(tooltipItemsElement, 'gap', '4px'); |
|||
this.renderer.appendChild(tooltipElement, tooltipItemsElement); |
|||
if (this.settings.tooltipShowDate) { |
|||
this.renderer.appendChild(tooltipItemsElement, this.constructTooltipDateElement(items[0].param, interval)); |
|||
} |
|||
for (const item of items) { |
|||
this.renderer.appendChild(tooltipItemsElement, this.constructTooltipSeriesElement(item)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private constructTooltipDateElement(param: CallbackDataParams, interval?: Interval): HTMLElement { |
|||
const dateElement: HTMLElement = this.renderer.createElement('div'); |
|||
let dateText: string; |
|||
const startTs = param.value[2]; |
|||
const endTs = param.value[3]; |
|||
if (this.settings.tooltipDateInterval && startTs && endTs && (endTs - 1) > startTs) { |
|||
const startDateText = this.tooltipDateFormat.update(startTs, interval); |
|||
const endDateText = this.tooltipDateFormat.update(endTs - 1, interval); |
|||
if (startDateText === endDateText) { |
|||
dateText = startDateText; |
|||
} else { |
|||
dateText = startDateText + ' - ' + endDateText; |
|||
} |
|||
} else { |
|||
const ts = param.value[0]; |
|||
dateText = this.tooltipDateFormat.update(ts, interval); |
|||
} |
|||
this.renderer.appendChild(dateElement, this.renderer.createText(dateText)); |
|||
this.renderer.setStyle(dateElement, 'font-family', this.settings.tooltipDateFont.family); |
|||
this.renderer.setStyle(dateElement, 'font-size', this.settings.tooltipDateFont.size + this.settings.tooltipDateFont.sizeUnit); |
|||
this.renderer.setStyle(dateElement, 'font-style', this.settings.tooltipDateFont.style); |
|||
this.renderer.setStyle(dateElement, 'font-weight', this.settings.tooltipDateFont.weight); |
|||
this.renderer.setStyle(dateElement, 'line-height', this.settings.tooltipDateFont.lineHeight); |
|||
this.renderer.setStyle(dateElement, 'color', this.settings.tooltipDateColor); |
|||
return dateElement; |
|||
} |
|||
|
|||
private constructTooltipSeriesElement(item: TooltipItem): HTMLElement { |
|||
const labelValueElement: HTMLElement = this.renderer.createElement('div'); |
|||
this.renderer.setStyle(labelValueElement, 'display', 'flex'); |
|||
this.renderer.setStyle(labelValueElement, 'flex-direction', 'row'); |
|||
this.renderer.setStyle(labelValueElement, 'align-items', 'center'); |
|||
this.renderer.setStyle(labelValueElement, 'align-self', 'stretch'); |
|||
this.renderer.setStyle(labelValueElement, 'gap', '12px'); |
|||
const labelElement: HTMLElement = this.renderer.createElement('div'); |
|||
this.renderer.setStyle(labelElement, 'display', 'flex'); |
|||
this.renderer.setStyle(labelElement, 'align-items', 'center'); |
|||
this.renderer.setStyle(labelElement, 'gap', '8px'); |
|||
this.renderer.appendChild(labelValueElement, labelElement); |
|||
const circleElement: HTMLElement = this.renderer.createElement('div'); |
|||
this.renderer.setStyle(circleElement, 'width', '8px'); |
|||
this.renderer.setStyle(circleElement, 'height', '8px'); |
|||
this.renderer.setStyle(circleElement, 'border-radius', '50%'); |
|||
this.renderer.setStyle(circleElement, 'background', item.param.color); |
|||
this.renderer.appendChild(labelElement, circleElement); |
|||
const labelTextElement: HTMLElement = this.renderer.createElement('div'); |
|||
this.renderer.setProperty(labelTextElement, 'innerHTML', this.sanitizer.sanitize(SecurityContext.HTML, item.param.seriesName)); |
|||
this.renderer.setStyle(labelTextElement, 'font-family', this.settings.tooltipLabelFont.family); |
|||
this.renderer.setStyle(labelTextElement, 'font-size', this.settings.tooltipLabelFont.size + this.settings.tooltipLabelFont.sizeUnit); |
|||
this.renderer.setStyle(labelTextElement, 'font-style', this.settings.tooltipLabelFont.style); |
|||
this.renderer.setStyle(labelTextElement, 'font-weight', this.settings.tooltipLabelFont.weight); |
|||
this.renderer.setStyle(labelTextElement, 'line-height', this.settings.tooltipLabelFont.lineHeight); |
|||
this.renderer.setStyle(labelTextElement, 'color', this.settings.tooltipLabelColor); |
|||
this.renderer.appendChild(labelElement, labelTextElement); |
|||
const valueElement: HTMLElement = this.renderer.createElement('div'); |
|||
let formatFunction = this.valueFormatFunction; |
|||
let latestData: FormattedData; |
|||
let units = ''; |
|||
let decimals = 0; |
|||
if (item.dataItem) { |
|||
if (item.dataItem.tooltipValueFormatFunction) { |
|||
formatFunction = item.dataItem.tooltipValueFormatFunction; |
|||
} |
|||
latestData = item.dataItem.latestData; |
|||
units = item.dataItem.units; |
|||
decimals = item.dataItem.decimals; |
|||
} |
|||
if (!latestData) { |
|||
latestData = {} as FormattedData; |
|||
} |
|||
const value = formatFunction(item.param.value[1], latestData, units, decimals); |
|||
this.renderer.setProperty(valueElement, 'innerHTML', this.sanitizer.sanitize(SecurityContext.HTML, value)); |
|||
this.renderer.setStyle(valueElement, 'flex', '1'); |
|||
this.renderer.setStyle(valueElement, 'text-align', 'end'); |
|||
this.renderer.setStyle(valueElement, 'font-family', this.settings.tooltipValueFont.family); |
|||
this.renderer.setStyle(valueElement, 'font-size', this.settings.tooltipValueFont.size + this.settings.tooltipValueFont.sizeUnit); |
|||
this.renderer.setStyle(valueElement, 'font-style', this.settings.tooltipValueFont.style); |
|||
this.renderer.setStyle(valueElement, 'font-weight', this.settings.tooltipValueFont.weight); |
|||
this.renderer.setStyle(valueElement, 'line-height', this.settings.tooltipValueFont.lineHeight); |
|||
this.renderer.setStyle(valueElement, 'color', this.settings.tooltipValueColor); |
|||
this.renderer.appendChild(labelValueElement, valueElement); |
|||
return labelValueElement; |
|||
} |
|||
|
|||
private static mapTooltipParams(params: CallbackDataParams[] | CallbackDataParams, |
|||
series?: TimeSeriesChartDataItem[], |
|||
focusedSeriesIndex?: number): TooltipParams { |
|||
const result: TooltipParams = { |
|||
items: [], |
|||
comparisonItems: [] |
|||
}; |
|||
if (!params || Array.isArray(params) && !params[0]) { |
|||
return result; |
|||
} |
|||
const firstParam = Array.isArray(params) ? params[0] : params; |
|||
if (!firstParam.value) { |
|||
return result; |
|||
} |
|||
let seriesParams: CallbackDataParams = null; |
|||
if (Array.isArray(params) && focusedSeriesIndex > -1) { |
|||
seriesParams = params.find(param => param.seriesIndex === focusedSeriesIndex); |
|||
} else if (!Array.isArray(params)) { |
|||
seriesParams = params; |
|||
} |
|||
if (seriesParams) { |
|||
TimeSeriesChartTooltip.appendTooltipItem(result, seriesParams, series); |
|||
} else if (Array.isArray(params)) { |
|||
for (seriesParams of params) { |
|||
TimeSeriesChartTooltip.appendTooltipItem(result, seriesParams, series); |
|||
} |
|||
} |
|||
return result; |
|||
} |
|||
|
|||
private static appendTooltipItem(tooltipParams: TooltipParams, seriesParams: CallbackDataParams, series?: TimeSeriesChartDataItem[]) { |
|||
const dataItem = series?.find(s => s.id === seriesParams.seriesId); |
|||
const tooltipItem: TooltipItem = { |
|||
param: seriesParams, |
|||
dataItem |
|||
}; |
|||
if (dataItem?.comparisonItem) { |
|||
tooltipParams.comparisonItems.push(tooltipItem); |
|||
} else { |
|||
tooltipParams.items.push(tooltipItem); |
|||
} |
|||
}; |
|||
} |
|||
@ -0,0 +1,79 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2024 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. |
|||
|
|||
--> |
|||
<ng-container [formGroup]="unreadNotificationWidgetSettingsForm"> |
|||
<div class="tb-form-panel"> |
|||
<div class="tb-form-panel-title" translate>widget-config.appearance</div> |
|||
<div class="tb-form-row space-between"> |
|||
<div>{{ 'widgets.notification.max-notification-display' | translate }}</div> |
|||
<mat-form-field appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput type="number" min="1" formControlName="maxNotificationDisplay" placeholder="{{ 'widget-config.set' | translate }}"> |
|||
</mat-form-field> |
|||
</div> |
|||
<div class="tb-form-panel"> |
|||
<div class="tb-form-panel-title" translate>widget-config.appearance</div> |
|||
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="enableFilter"> |
|||
{{ 'widgets.notification.type-filter' | translate }} |
|||
</mat-slide-toggle> |
|||
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="enableMarkAsRead"> |
|||
{{ 'widgets.notification.button-mark-read' | translate }} |
|||
</mat-slide-toggle> |
|||
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="enableViewAll"> |
|||
{{ 'widgets.notification.button-view-all' | translate }} |
|||
</mat-slide-toggle> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="tb-form-panel"> |
|||
<div class="tb-form-row no-padding no-border"> |
|||
<mat-slide-toggle class="mat-slide" formControlName="showCounter"> |
|||
{{ 'widgets.notification.counter' | translate }} |
|||
</mat-slide-toggle> |
|||
</div> |
|||
<div class="tb-form-row space-between"> |
|||
<div>{{ 'widgets.notification.counter-value' | translate }}</div> |
|||
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
|||
<tb-font-settings formControlName="counterValueFont" |
|||
clearButton |
|||
[previewText]="unreadNotificationWidgetSettingsForm.get('maxNotificationDisplay').value.toString()" |
|||
[initialPreviewStyle]="widgetConfig.config.titleStyle"> |
|||
</tb-font-settings> |
|||
<tb-color-input asBoxInput |
|||
formControlName="counterValueColor"> |
|||
</tb-color-input> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row space-between"> |
|||
<div>{{ 'widgets.notification.counter-color' | translate }}</div> |
|||
<tb-color-input asBoxInput |
|||
formControlName="counterColor"> |
|||
</tb-color-input> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="tb-form-row space-between"> |
|||
<div>{{ 'widgets.background.background' | translate }}</div> |
|||
<tb-background-settings formControlName="background"> |
|||
</tb-background-settings> |
|||
</div> |
|||
<div class="tb-form-row space-between"> |
|||
<div>{{ 'widget-config.card-padding' | translate }}</div> |
|||
<mat-form-field appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput formControlName="padding" placeholder="{{ 'widget-config.set' | translate }}"> |
|||
</mat-form-field> |
|||
</div> |
|||
</ng-container> |
|||
@ -0,0 +1,81 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 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 { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; |
|||
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { unreadNotificationDefaultSettings } from '@home/components/widget/lib/cards/unread-notification-widget.models'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-unread-notification-widget-settings', |
|||
templateUrl: './unread-notification-widget-settings.component.html', |
|||
styleUrls: ['./../widget-settings.scss'] |
|||
}) |
|||
export class UnreadNotificationWidgetSettingsComponent extends WidgetSettingsComponent { |
|||
|
|||
unreadNotificationWidgetSettingsForm: UntypedFormGroup; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
private fb: UntypedFormBuilder) { |
|||
super(store); |
|||
} |
|||
|
|||
protected settingsForm(): UntypedFormGroup { |
|||
return this.unreadNotificationWidgetSettingsForm; |
|||
} |
|||
|
|||
protected defaultSettings(): WidgetSettings { |
|||
return {...unreadNotificationDefaultSettings}; |
|||
} |
|||
|
|||
protected onSettingsSet(settings: WidgetSettings) { |
|||
this.unreadNotificationWidgetSettingsForm = this.fb.group({ |
|||
maxNotificationDisplay: [settings?.maxNotificationDisplay, [Validators.required, Validators.min(1)]], |
|||
showCounter: [settings?.showCounter, []], |
|||
counterValueFont: [settings?.counterValueFont, []], |
|||
counterValueColor: [settings?.counterValueColor, []], |
|||
counterColor: [settings?.counterColor, []], |
|||
|
|||
enableViewAll: [settings?.enableViewAll, []], |
|||
enableFilter: [settings?.enableFilter, []], |
|||
enableMarkAsRead: [settings?.enableMarkAsRead, []], |
|||
|
|||
background: [settings?.background, []], |
|||
padding: [settings.padding, []] |
|||
}); |
|||
} |
|||
|
|||
protected validatorTriggers(): string[] { |
|||
return ['showCounter']; |
|||
} |
|||
|
|||
protected updateValidators(emitEvent: boolean) { |
|||
const showCounter: boolean = this.unreadNotificationWidgetSettingsForm.get('showCounter').value; |
|||
|
|||
if (showCounter) { |
|||
this.unreadNotificationWidgetSettingsForm.get('counterValueFont').enable({emitEvent}); |
|||
this.unreadNotificationWidgetSettingsForm.get('counterValueColor').enable({emitEvent}); |
|||
this.unreadNotificationWidgetSettingsForm.get('counterColor').enable({emitEvent}); |
|||
} else { |
|||
this.unreadNotificationWidgetSettingsForm.get('counterValueFont').disable({emitEvent}); |
|||
this.unreadNotificationWidgetSettingsForm.get('counterValueColor').disable({emitEvent}); |
|||
this.unreadNotificationWidgetSettingsForm.get('counterColor').disable({emitEvent}); |
|||
} |
|||
} |
|||
|
|||
} |
|||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 3.0 KiB |
Loading…
Reference in new issue