57 changed files with 1217 additions and 729 deletions
@ -0,0 +1,234 @@ |
|||
<!-- |
|||
|
|||
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-form-panel no-padding no-border" [formGroup]="widgetActionFormGroup"> |
|||
<div class="tb-form-row"> |
|||
<div translate>widget-config.action</div> |
|||
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-select required formControlName="type" placeholder="{{ 'widget-config.set' | translate }}"> |
|||
<mat-option *ngFor="let actionType of widgetActionTypes" [value]="actionType"> |
|||
{{ widgetActionTypeTranslations.get(widgetActionType[actionType]) | translate }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
</div> |
|||
<ng-container [formGroup]="actionTypeFormGroup" [ngSwitch]="widgetActionFormGroup.get('type').value"> |
|||
<ng-template [ngSwitchCase]="widgetActionType.openDashboard"> |
|||
<div class="tb-form-row"> |
|||
<div class="fixed-title-width">{{ 'widget-action.target-dashboard' | translate }}*</div> |
|||
<tb-dashboard-autocomplete fxFlex |
|||
formControlName="targetDashboardId" |
|||
required |
|||
requiredText="widget-action.target-dashboard-required" |
|||
placeholder="{{ 'widget-action.select-target-dashboard' | translate }}" |
|||
inlineField |
|||
[selectFirstDashboard]="true" |
|||
></tb-dashboard-autocomplete> |
|||
</div> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState || |
|||
widgetActionFormGroup.get('type').value === widgetActionType.updateDashboardState || |
|||
widgetActionFormGroup.get('type').value === widgetActionType.openDashboard ? |
|||
widgetActionFormGroup.get('type').value : ''"> |
|||
<div class="tb-form-row"> |
|||
<div class="fixed-title-width">{{ 'widget-action.target-dashboard-state' | translate }} |
|||
{{widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState ? '*' : ''}}</div> |
|||
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic" |
|||
[class.tb-suffix-absolute]="!actionTypeFormGroup.get('targetDashboardStateId').value"> |
|||
<input matInput type="text" placeholder="{{ 'widget-action.target-dashboard-state' | translate }}" |
|||
#dashboardStateInput |
|||
formControlName="targetDashboardStateId" |
|||
[required]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState" |
|||
(focusin)="onDashboardStateInputFocus()" |
|||
[matAutocomplete]="targetDashboardStateAutocomplete"> |
|||
<button *ngIf="actionTypeFormGroup.get('targetDashboardStateId').value" |
|||
type="button" |
|||
matSuffix mat-icon-button aria-label="Clear" |
|||
(click)="clearTargetDashboardState()"> |
|||
<mat-icon class="material-icons">close</mat-icon> |
|||
</button> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="'widget-action.target-dashboard-state-required' | translate" |
|||
*ngIf="actionTypeFormGroup.get('targetDashboardStateId').hasError('required') |
|||
&& actionTypeFormGroup.get('targetDashboardStateId').touched" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
<mat-autocomplete |
|||
class="tb-autocomplete" |
|||
#targetDashboardStateAutocomplete="matAutocomplete"> |
|||
<mat-option *ngFor="let state of filteredDashboardStates | async" [value]="state"> |
|||
<span [innerHTML]="state | highlight:targetDashboardStateSearchText"></span> |
|||
</mat-option> |
|||
</mat-autocomplete> |
|||
</mat-form-field> |
|||
</div> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState || |
|||
widgetActionFormGroup.get('type').value === widgetActionType.updateDashboardState ? |
|||
widgetActionFormGroup.get('type').value : ''"> |
|||
<div class="tb-form-row"> |
|||
<mat-slide-toggle class="mat-slide" formControlName="openRightLayout"> |
|||
{{ 'widget-action.open-right-layout' | translate }} |
|||
</mat-slide-toggle> |
|||
</div> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboard ? |
|||
widgetActionFormGroup.get('type').value : ''"> |
|||
<div class="tb-form-row"> |
|||
<mat-slide-toggle class="mat-slide" formControlName="openNewBrowserTab"> |
|||
{{ 'widget-action.open-new-browser-tab' | translate }} |
|||
</mat-slide-toggle> |
|||
</div> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState || |
|||
widgetActionFormGroup.get('type').value === widgetActionType.updateDashboardState || |
|||
widgetActionFormGroup.get('type').value === widgetActionType.openDashboard ? |
|||
widgetActionFormGroup.get('type').value : ''"> |
|||
<div class="tb-form-row" *ngIf="widgetType !== WidgetType.static"> |
|||
<mat-slide-toggle class="mat-slide" formControlName="setEntityId"> |
|||
{{ 'widget-action.set-entity-from-widget' | translate }} |
|||
</mat-slide-toggle> |
|||
</div> |
|||
<div class="tb-form-row" *ngIf="actionTypeFormGroup.get('setEntityId').value"> |
|||
<div class="fixed-title-width">{{ 'alias.state-entity-parameter-name' | translate }}</div> |
|||
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput |
|||
placeholder="{{ 'alias.default-entity-parameter-name' | translate }}" |
|||
formControlName="stateEntityParamName"> |
|||
</mat-form-field> |
|||
</div> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState ? |
|||
widgetActionFormGroup.get('type').value : ''"> |
|||
<div class="tb-form-row"> |
|||
<div class="fixed-title-width">{{ 'widget-action.state-display-type' | translate }}</div> |
|||
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-select formControlName="stateDisplayType"> |
|||
<mat-option *ngFor="let displayType of allStateDisplayTypes" [value]="displayType"> |
|||
{{ stateDisplayTypeName(displayType) }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
</div> |
|||
<ng-container [formGroup]="stateDisplayTypeFormGroup" [ngSwitch]="actionTypeFormGroup.get('stateDisplayType').value"> |
|||
<ng-template [ngSwitchCase]="'separateDialog'"> |
|||
<div class="tb-form-row"> |
|||
<div class="fixed-title-width">{{ 'widget-action.dialog-title' | translate }}</div> |
|||
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput placeholder="{{ 'widget-config.set' | translate }}" formControlName="dialogTitle"> |
|||
</mat-form-field> |
|||
</div> |
|||
<div class="tb-form-row"> |
|||
<mat-slide-toggle class="mat-slide" formControlName="dialogHideDashboardToolbar"> |
|||
{{ 'widget-action.dialog-hide-dashboard-toolbar' | translate }} |
|||
</mat-slide-toggle> |
|||
</div> |
|||
<div class="tb-form-row space-between"> |
|||
<div>{{ 'widget-action.dialog-width' | translate }}</div> |
|||
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="tb-suffix-absolute number"> |
|||
<input type="number" min="1" max="100" step="1" matInput formControlName="dialogWidth"> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="'widget-action.dialog-size-range-error' | translate" |
|||
*ngIf="stateDisplayTypeFormGroup.get('dialogWidth').invalid && stateDisplayTypeFormGroup.get('dialogWidth').touched" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
</div> |
|||
<div class="tb-form-row space-between"> |
|||
<div>{{ 'widget-action.dialog-height' | translate }}</div> |
|||
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="tb-suffix-absolute number"> |
|||
<input type="number" min="1" max="100" step="1" matInput formControlName="dialogHeight"> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="'widget-action.dialog-size-range-error' | translate" |
|||
*ngIf="stateDisplayTypeFormGroup.get('dialogHeight').invalid && stateDisplayTypeFormGroup.get('dialogHeight').touched" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
</div> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="'popover'"> |
|||
<div class="tb-form-row"> |
|||
<div class="fixed-title-width">{{ 'widget-action.popover-preferred-placement' | translate }}</div> |
|||
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-select formControlName="popoverPreferredPlacement"> |
|||
<mat-option *ngFor="let placement of allPopoverPlacements" [value]="placement"> |
|||
{{ popoverPlacementName(placement) }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
</div> |
|||
<div class="tb-form-row"> |
|||
<mat-slide-toggle class="mat-slide" formControlName="popoverHideOnClickOutside"> |
|||
{{ 'widget-action.popover-hide-on-click-outside' | translate }} |
|||
</mat-slide-toggle> |
|||
</div> |
|||
<div class="tb-form-row"> |
|||
<mat-slide-toggle class="mat-slide" formControlName="popoverHideDashboardToolbar"> |
|||
{{ 'widget-action.popover-hide-dashboard-toolbar' | translate }} |
|||
</mat-slide-toggle> |
|||
</div> |
|||
<div class="tb-form-row space-between"> |
|||
<div>{{ 'widget-action.popover-width' | translate }}</div> |
|||
<tb-css-size-input formControlName="popoverWidth"> |
|||
</tb-css-size-input> |
|||
</div> |
|||
<div class="tb-form-row space-between"> |
|||
<div>{{ 'widget-action.popover-height' | translate }}</div> |
|||
<tb-css-size-input formControlName="popoverHeight"> |
|||
</tb-css-size-input> |
|||
</div> |
|||
<tb-json-object-edit |
|||
[editorStyle]="{minHeight: '100px'}" |
|||
label="{{ 'widget-action.popover-style' | translate }}" |
|||
formControlName="popoverStyle" |
|||
></tb-json-object-edit> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="'normal'"> |
|||
</ng-template> |
|||
</ng-container> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="widgetActionType.custom"> |
|||
<tb-js-func |
|||
formControlName="customFunction" |
|||
functionTitle="{{ 'widget-action.custom-action-function' | translate }}" |
|||
[functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']" |
|||
[globalVariables]="functionScopeVariables" |
|||
[validationArgs]="[]" |
|||
[editorCompleter]="customActionEditorCompleter" |
|||
helpId="widget/action/custom_action_fn" |
|||
></tb-js-func> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="widgetActionType.customPretty"> |
|||
<tb-custom-action-pretty-editor |
|||
formControlName="customAction"> |
|||
</tb-custom-action-pretty-editor> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="widgetActionType.mobileAction"> |
|||
<tb-mobile-action-editor formControlName="mobileAction"> |
|||
</tb-mobile-action-editor> |
|||
</ng-template> |
|||
</ng-container> |
|||
</div> |
|||
@ -0,0 +1,463 @@ |
|||
///
|
|||
/// 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 { |
|||
ControlValueAccessor, |
|||
NG_VALIDATORS, |
|||
NG_VALUE_ACCESSOR, |
|||
UntypedFormBuilder, |
|||
UntypedFormControl, |
|||
UntypedFormGroup, |
|||
Validator, |
|||
Validators |
|||
} from '@angular/forms'; |
|||
import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; |
|||
import { |
|||
WidgetAction, |
|||
WidgetActionType, |
|||
widgetActionTypeTranslationMap, |
|||
widgetType |
|||
} from '@shared/models/widget.models'; |
|||
import { WidgetService } from '@core/http/widget.service'; |
|||
import { WidgetActionCallbacks } from '@home/components/widget/action/manage-widget-actions.component.models'; |
|||
import { map, mergeMap, share, startWith, takeUntil, tap } from 'rxjs/operators'; |
|||
import { Observable, of, Subject, Subscription } from 'rxjs'; |
|||
import { Dashboard } from '@shared/models/dashboard.models'; |
|||
import { DashboardService } from '@core/http/dashboard.service'; |
|||
import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; |
|||
import { isDefinedAndNotNull } from '@core/utils'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { PopoverPlacement, PopoverPlacements } from '@shared/components/popover.models'; |
|||
import { |
|||
CustomActionEditorCompleter, |
|||
toCustomAction |
|||
} from '@home/components/widget/config/action/custom-action.models'; |
|||
|
|||
const stateDisplayTypes = ['normal', 'separateDialog', 'popover'] as const; |
|||
type stateDisplayTypeTuple = typeof stateDisplayTypes; |
|||
export type stateDisplayType = stateDisplayTypeTuple[number]; |
|||
|
|||
const stateDisplayTypesTranslations = new Map<stateDisplayType, string>( |
|||
[ |
|||
['normal', 'widget-action.open-normal'], |
|||
['separateDialog', 'widget-action.open-in-separate-dialog'], |
|||
['popover', 'widget-action.open-in-popover'], |
|||
] |
|||
); |
|||
|
|||
@Component({ |
|||
selector: 'tb-widget-action', |
|||
templateUrl: './widget-action.component.html', |
|||
styleUrls: [], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => WidgetActionComponent), |
|||
multi: true |
|||
}, |
|||
{ |
|||
provide: NG_VALIDATORS, |
|||
useExisting: forwardRef(() => WidgetActionComponent), |
|||
multi: true, |
|||
} |
|||
] |
|||
}) |
|||
export class WidgetActionComponent implements ControlValueAccessor, OnInit, Validator { |
|||
|
|||
@ViewChild('dashboardStateInput', {static: false}) dashboardStateInput: ElementRef; |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
@Input() |
|||
widgetType: widgetType; |
|||
|
|||
@Input() |
|||
callbacks: WidgetActionCallbacks; |
|||
|
|||
widgetActionTypes = Object.keys(WidgetActionType); |
|||
widgetActionTypeTranslations = widgetActionTypeTranslationMap; |
|||
widgetActionType = WidgetActionType; |
|||
|
|||
allStateDisplayTypes = stateDisplayTypes; |
|||
allPopoverPlacements = PopoverPlacements; |
|||
|
|||
WidgetType = widgetType; |
|||
|
|||
filteredDashboardStates: Observable<Array<string>>; |
|||
targetDashboardStateSearchText = ''; |
|||
selectedDashboardStateIds: Observable<Array<string>>; |
|||
|
|||
customActionEditorCompleter = CustomActionEditorCompleter; |
|||
|
|||
functionScopeVariables = this.widgetService.getWidgetScopeVariables(); |
|||
|
|||
widgetActionFormGroup: UntypedFormGroup; |
|||
actionTypeFormGroup: UntypedFormGroup; |
|||
stateDisplayTypeFormGroup: UntypedFormGroup; |
|||
|
|||
private propagateChange = (_val: any) => {}; |
|||
private actionTypeFormGroupSubscriptions: Subscription[] = []; |
|||
private stateDisplayTypeFormGroupSubscriptions: Subscription[] = []; |
|||
private destroy$ = new Subject<void>(); |
|||
private dashboard: Dashboard; |
|||
|
|||
constructor(private fb: UntypedFormBuilder, |
|||
private widgetService: WidgetService, |
|||
private dashboardService: DashboardService, |
|||
private dashboardUtils: DashboardUtilsService, |
|||
private translate: TranslateService) { |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(_fn: any): void { |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (this.disabled) { |
|||
this.widgetActionFormGroup.disable({emitEvent: false}); |
|||
if (this.actionTypeFormGroup) { |
|||
this.actionTypeFormGroup.disable({emitEvent: false}); |
|||
} |
|||
if (this.stateDisplayTypeFormGroup) { |
|||
this.stateDisplayTypeFormGroup.disable({emitEvent: false}); |
|||
} |
|||
} else { |
|||
this.widgetActionFormGroup.enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
ngOnInit() { |
|||
this.widgetActionFormGroup = this.fb.group({}); |
|||
this.widgetActionFormGroup.addControl('type', |
|||
this.fb.control(null, [Validators.required])); |
|||
this.widgetActionFormGroup.get('type').valueChanges.pipe( |
|||
takeUntil(this.destroy$) |
|||
).subscribe((type: WidgetActionType) => { |
|||
this.updateActionTypeFormGroup(type); |
|||
}); |
|||
this.widgetActionFormGroup.valueChanges.pipe( |
|||
takeUntil(this.destroy$) |
|||
).subscribe(() => { |
|||
this.widgetActionUpdated(); |
|||
}); |
|||
} |
|||
|
|||
writeValue(widgetAction?: WidgetAction): void { |
|||
this.widgetActionFormGroup.patchValue({ |
|||
type: widgetAction?.type |
|||
}, {emitEvent: false}); |
|||
this.updateActionTypeFormGroup(widgetAction?.type, widgetAction); |
|||
} |
|||
|
|||
validate(_c: UntypedFormControl) { |
|||
return (this.widgetActionFormGroup.valid && |
|||
this.actionTypeFormGroup.valid && (!this.stateDisplayTypeFormGroup || this.stateDisplayTypeFormGroup.valid)) ? null : { |
|||
widgetAction: { |
|||
valid: false, |
|||
} |
|||
}; |
|||
} |
|||
|
|||
clearTargetDashboardState(value: string = '') { |
|||
this.dashboardStateInput.nativeElement.value = value; |
|||
this.actionTypeFormGroup.get('targetDashboardStateId').patchValue(value, {emitEvent: true}); |
|||
setTimeout(() => { |
|||
this.dashboardStateInput.nativeElement.blur(); |
|||
this.dashboardStateInput.nativeElement.focus(); |
|||
}, 0); |
|||
} |
|||
|
|||
onDashboardStateInputFocus(): void { |
|||
this.actionTypeFormGroup.get('targetDashboardStateId').updateValueAndValidity({onlySelf: true, emitEvent: true}); |
|||
} |
|||
|
|||
stateDisplayTypeName(displayType: stateDisplayType): string { |
|||
if (displayType) { |
|||
return this.translate.instant(stateDisplayTypesTranslations.get(displayType)) + ''; |
|||
} else { |
|||
return ''; |
|||
} |
|||
} |
|||
|
|||
popoverPlacementName(placement: PopoverPlacement): string { |
|||
if (placement) { |
|||
return this.translate.instant(`widget-action.popover-placement-${placement}`) + ''; |
|||
} else { |
|||
return ''; |
|||
} |
|||
} |
|||
|
|||
private updateActionTypeFormGroup(type?: WidgetActionType, action?: WidgetAction) { |
|||
this.actionTypeFormGroupSubscriptions.forEach(s => s.unsubscribe()); |
|||
this.actionTypeFormGroupSubscriptions.length = 0; |
|||
this.actionTypeFormGroup = this.fb.group({}); |
|||
if (type) { |
|||
switch (type) { |
|||
case WidgetActionType.openDashboard: |
|||
case WidgetActionType.openDashboardState: |
|||
case WidgetActionType.updateDashboardState: |
|||
this.actionTypeFormGroup.addControl( |
|||
'targetDashboardStateId', |
|||
this.fb.control(action ? action.targetDashboardStateId : null, |
|||
type === WidgetActionType.openDashboardState ? [Validators.required] : []) |
|||
); |
|||
this.actionTypeFormGroup.addControl( |
|||
'setEntityId', |
|||
this.fb.control(this.widgetType === widgetType.static ? false : action ? action.setEntityId : true, []) |
|||
); |
|||
this.actionTypeFormGroup.addControl( |
|||
'stateEntityParamName', |
|||
this.fb.control(action ? action.stateEntityParamName : null, []) |
|||
); |
|||
if (type === WidgetActionType.openDashboard) { |
|||
this.actionTypeFormGroup.addControl( |
|||
'openNewBrowserTab', |
|||
this.fb.control(action ? action.openNewBrowserTab : false, []) |
|||
); |
|||
this.actionTypeFormGroup.addControl( |
|||
'targetDashboardId', |
|||
this.fb.control(action ? action.targetDashboardId : null, |
|||
[Validators.required]) |
|||
); |
|||
this.setupSelectedDashboardStateIds(); |
|||
} else { |
|||
if (type === WidgetActionType.openDashboardState) { |
|||
const displayType = this.getStateDisplayType(action); |
|||
this.actionTypeFormGroup.addControl( |
|||
'stateDisplayType', |
|||
this.fb.control(this.getStateDisplayType(action), [Validators.required]) |
|||
); |
|||
this.updateStateDisplayTypeFormGroup(displayType, action); |
|||
this.actionTypeFormGroupSubscriptions.push( |
|||
this.actionTypeFormGroup.get('stateDisplayType').valueChanges.pipe( |
|||
takeUntil(this.destroy$) |
|||
).subscribe((displayTypeValue: stateDisplayType) => { |
|||
this.updateStateDisplayTypeFormGroup(displayTypeValue); |
|||
}) |
|||
); |
|||
} |
|||
this.actionTypeFormGroup.addControl( |
|||
'openRightLayout', |
|||
this.fb.control(action ? action.openRightLayout : false, []) |
|||
); |
|||
} |
|||
this.setupFilteredDashboardStates(); |
|||
break; |
|||
case WidgetActionType.custom: |
|||
this.actionTypeFormGroup.addControl( |
|||
'customFunction', |
|||
this.fb.control(action ? action.customFunction : null, []) |
|||
); |
|||
break; |
|||
case WidgetActionType.customPretty: |
|||
this.actionTypeFormGroup.addControl( |
|||
'customAction', |
|||
this.fb.control(toCustomAction(action), [Validators.required]) |
|||
); |
|||
break; |
|||
case WidgetActionType.mobileAction: |
|||
this.actionTypeFormGroup.addControl( |
|||
'mobileAction', |
|||
this.fb.control(action ? action.mobileAction : null, [Validators.required]) |
|||
); |
|||
break; |
|||
} |
|||
} |
|||
this.actionTypeFormGroupSubscriptions.push( |
|||
this.actionTypeFormGroup.valueChanges.pipe( |
|||
takeUntil(this.destroy$) |
|||
).subscribe(() => { |
|||
this.widgetActionUpdated(); |
|||
}) |
|||
); |
|||
} |
|||
|
|||
private updateStateDisplayTypeFormGroup(displayType?: stateDisplayType, action?: WidgetAction) { |
|||
this.stateDisplayTypeFormGroupSubscriptions.forEach(s => s.unsubscribe()); |
|||
this.stateDisplayTypeFormGroupSubscriptions.length = 0; |
|||
this.stateDisplayTypeFormGroup = this.fb.group({}); |
|||
if (displayType) { |
|||
switch (displayType) { |
|||
case 'normal': |
|||
break; |
|||
case 'separateDialog': |
|||
this.stateDisplayTypeFormGroup.addControl( |
|||
'dialogTitle', |
|||
this.fb.control(action ? action.dialogTitle : '', []) |
|||
); |
|||
this.stateDisplayTypeFormGroup.addControl( |
|||
'dialogHideDashboardToolbar', |
|||
this.fb.control(action && isDefinedAndNotNull(action.dialogHideDashboardToolbar) |
|||
? action.dialogHideDashboardToolbar : true, []) |
|||
); |
|||
this.stateDisplayTypeFormGroup.addControl( |
|||
'dialogWidth', |
|||
this.fb.control(action ? action.dialogWidth : null, [Validators.min(1), Validators.max(100)]) |
|||
); |
|||
this.stateDisplayTypeFormGroup.addControl( |
|||
'dialogHeight', |
|||
this.fb.control(action ? action.dialogHeight : null, [Validators.min(1), Validators.max(100)]) |
|||
); |
|||
break; |
|||
case 'popover': |
|||
this.stateDisplayTypeFormGroup.addControl( |
|||
'popoverPreferredPlacement', |
|||
this.fb.control(action && isDefinedAndNotNull(action.popoverPreferredPlacement) |
|||
? action.popoverPreferredPlacement : 'top', []) |
|||
); |
|||
this.stateDisplayTypeFormGroup.addControl( |
|||
'popoverHideOnClickOutside', |
|||
this.fb.control(action && isDefinedAndNotNull(action.popoverHideOnClickOutside) |
|||
? action.popoverHideOnClickOutside : true, []) |
|||
); |
|||
this.stateDisplayTypeFormGroup.addControl( |
|||
'popoverHideDashboardToolbar', |
|||
this.fb.control(action && isDefinedAndNotNull(action.popoverHideDashboardToolbar) |
|||
? action.popoverHideDashboardToolbar : true, []) |
|||
); |
|||
this.stateDisplayTypeFormGroup.addControl( |
|||
'popoverWidth', |
|||
this.fb.control(action && isDefinedAndNotNull(action.popoverWidth) ? action.popoverWidth : '25vw', []) |
|||
); |
|||
this.stateDisplayTypeFormGroup.addControl( |
|||
'popoverHeight', |
|||
this.fb.control(action && isDefinedAndNotNull(action.popoverHeight) ? action.popoverHeight : '25vh', []) |
|||
); |
|||
this.stateDisplayTypeFormGroup.addControl( |
|||
'popoverStyle', |
|||
this.fb.control(action && isDefinedAndNotNull(action.popoverStyle) ? action.popoverStyle : {}, []) |
|||
); |
|||
break; |
|||
} |
|||
} |
|||
this.stateDisplayTypeFormGroupSubscriptions.push( |
|||
this.stateDisplayTypeFormGroup.valueChanges.pipe( |
|||
takeUntil(this.destroy$) |
|||
).subscribe(() => { |
|||
this.widgetActionUpdated(); |
|||
}) |
|||
); |
|||
} |
|||
|
|||
private setupSelectedDashboardStateIds() { |
|||
this.selectedDashboardStateIds = |
|||
this.actionTypeFormGroup.get('targetDashboardId').valueChanges.pipe( |
|||
tap((dashboardId) => { |
|||
if (!dashboardId) { |
|||
this.actionTypeFormGroup.get('targetDashboardStateId') |
|||
.patchValue('', {emitEvent: true}); |
|||
} |
|||
|
|||
this.targetDashboardStateSearchText = ''; |
|||
}), |
|||
mergeMap((dashboardId) => { |
|||
if (dashboardId) { |
|||
if (this.dashboard?.id.id === dashboardId) { |
|||
return of(this.dashboard); |
|||
} else { |
|||
return this.dashboardService.getDashboard(dashboardId); |
|||
} |
|||
} else { |
|||
return of(null); |
|||
} |
|||
}), |
|||
map((dashboard: Dashboard) => { |
|||
if (dashboard) { |
|||
if (this.dashboard?.id.id !== dashboard.id.id) { |
|||
this.dashboard = this.dashboardUtils.validateAndUpdateDashboard(dashboard); |
|||
} |
|||
|
|||
return Object.keys(this.dashboard.configuration.states); |
|||
} else { |
|||
return []; |
|||
} |
|||
}), |
|||
share() |
|||
); |
|||
} |
|||
|
|||
private setupFilteredDashboardStates() { |
|||
this.targetDashboardStateSearchText = ''; |
|||
this.filteredDashboardStates = this.actionTypeFormGroup.get('targetDashboardStateId').valueChanges |
|||
.pipe( |
|||
startWith(''), |
|||
map(value => value ? value : ''), |
|||
mergeMap(name => this.fetchDashboardStates(name)), |
|||
takeUntil(this.destroy$) |
|||
); |
|||
} |
|||
|
|||
private fetchDashboardStates(searchText?: string): Observable<Array<string>> { |
|||
this.targetDashboardStateSearchText = searchText; |
|||
if (this.widgetActionFormGroup.get('type').value === WidgetActionType.openDashboard) { |
|||
return this.selectedDashboardStateIds.pipe( |
|||
map(stateIds => { |
|||
const result = searchText ? stateIds.filter(this.createFilterForDashboardState(searchText)) : stateIds; |
|||
if (result && result.length) { |
|||
return result; |
|||
} else { |
|||
return [searchText]; |
|||
} |
|||
}) |
|||
); |
|||
} else { |
|||
return of(this.callbacks.fetchDashboardStates(searchText)); |
|||
} |
|||
} |
|||
|
|||
private createFilterForDashboardState(query: string): (stateId: string) => boolean { |
|||
const lowercaseQuery = query.toLowerCase(); |
|||
return stateId => stateId.toLowerCase().indexOf(lowercaseQuery) === 0; |
|||
} |
|||
|
|||
private getStateDisplayType(action?: WidgetAction): stateDisplayType { |
|||
let res: stateDisplayType = 'normal'; |
|||
if (action) { |
|||
if (action.openInSeparateDialog) { |
|||
res = 'separateDialog'; |
|||
} else if (action.openInPopover) { |
|||
res = 'popover'; |
|||
} |
|||
} |
|||
return res; |
|||
} |
|||
|
|||
private widgetActionUpdated() { |
|||
const type: WidgetActionType = this.widgetActionFormGroup.get('type').value; |
|||
let result: WidgetAction; |
|||
if (type === WidgetActionType.customPretty) { |
|||
result = {...this.widgetActionFormGroup.value, ...this.actionTypeFormGroup.get('customAction').value}; |
|||
} else { |
|||
result = {...this.widgetActionFormGroup.value, ...this.actionTypeFormGroup.value}; |
|||
} |
|||
if (this.actionTypeFormGroup.get('stateDisplayType') && |
|||
this.actionTypeFormGroup.get('stateDisplayType').value !== 'normal') { |
|||
result = {...result, ...this.stateDisplayTypeFormGroup.value}; |
|||
result.openInSeparateDialog = this.actionTypeFormGroup.get('stateDisplayType').value === 'separateDialog'; |
|||
result.openInPopover = this.actionTypeFormGroup.get('stateDisplayType').value === 'popover'; |
|||
} else { |
|||
result.openInSeparateDialog = false; |
|||
result.openInPopover = false; |
|||
} |
|||
delete (result as any).stateDisplayType; |
|||
this.propagateChange(result); |
|||
} |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
<!-- |
|||
|
|||
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 [formGroup]="cssSizeFormGroup" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
|||
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="tb-inline-field tb-suffix-absolute number"> |
|||
<input matInput [required]="required" |
|||
type="number" min="0" formControlName="size" placeholder="{{ 'widget-config.set' | translate }}"> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="cssSizeFormGroup.get('size').hasError('required') ? ((requiredText ? requiredText : 'css-size.size-value-required') | translate) : |
|||
('css-size.invalid-size-value' | translate)" |
|||
*ngIf="cssSizeFormGroup.get('size').invalid && cssSizeFormGroup.get('size').touched" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
<tb-css-unit-select [allowEmpty]="allowEmptyUnit" width="" formControlName="unit"></tb-css-unit-select> |
|||
</div> |
|||
@ -0,0 +1,124 @@ |
|||
///
|
|||
/// 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, forwardRef, Input, OnInit } from '@angular/core'; |
|||
import { |
|||
ControlValueAccessor, |
|||
NG_VALIDATORS, |
|||
NG_VALUE_ACCESSOR, |
|||
UntypedFormBuilder, |
|||
UntypedFormControl, |
|||
UntypedFormGroup, |
|||
Validator, |
|||
Validators |
|||
} from '@angular/forms'; |
|||
import { cssUnit, resolveCssSize } from '@shared/models/widget-settings.models'; |
|||
import { coerceBoolean } from '@shared/decorators/coercion'; |
|||
import { isDefinedAndNotNull } from '@core/utils'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-css-size-input', |
|||
templateUrl: './css-size-input.component.html', |
|||
styleUrls: [], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => CssSizeInputComponent), |
|||
multi: true |
|||
}, |
|||
{ |
|||
provide: NG_VALIDATORS, |
|||
useExisting: forwardRef(() => CssSizeInputComponent), |
|||
multi: true, |
|||
} |
|||
] |
|||
}) |
|||
export class CssSizeInputComponent implements OnInit, ControlValueAccessor, Validator { |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
@Input() |
|||
@coerceBoolean() |
|||
required = false; |
|||
|
|||
@Input() |
|||
requiredText: string; |
|||
|
|||
@Input() |
|||
@coerceBoolean() |
|||
allowEmptyUnit = false; |
|||
|
|||
cssSizeFormGroup: UntypedFormGroup; |
|||
|
|||
modelValue: string; |
|||
|
|||
private propagateChange = null; |
|||
|
|||
constructor(private fb: UntypedFormBuilder) {} |
|||
|
|||
ngOnInit(): void { |
|||
this.cssSizeFormGroup = this.fb.group({ |
|||
size: [null, this.required ? [Validators.required, Validators.min(0)] : [Validators.min(0)]], |
|||
unit: [null, []] |
|||
}); |
|||
this.cssSizeFormGroup.valueChanges.subscribe((value: {size: number; unit: cssUnit}) => { |
|||
this.updateModel(value); |
|||
}); |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (this.disabled) { |
|||
this.cssSizeFormGroup.disable({emitEvent: false}); |
|||
} else { |
|||
this.cssSizeFormGroup.enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
writeValue(value: string): void { |
|||
this.modelValue = value; |
|||
const size = resolveCssSize(value); |
|||
this.cssSizeFormGroup.patchValue({ |
|||
size: size[0], |
|||
unit: size[1] |
|||
}, {emitEvent: false}); |
|||
} |
|||
|
|||
validate(_c: UntypedFormControl) { |
|||
return this.cssSizeFormGroup.valid ? null : { |
|||
cssSize: { |
|||
valid: false, |
|||
} |
|||
}; |
|||
} |
|||
|
|||
private updateModel(value: {size: number; unit: cssUnit}): void { |
|||
const result: string = isDefinedAndNotNull(value?.size) && isDefinedAndNotNull(value?.unit) |
|||
? value.size + value.unit : ''; |
|||
if (this.modelValue !== result) { |
|||
this.modelValue = result; |
|||
this.propagateChange(this.modelValue); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue