UI: Widget header button action

This commit is contained in:
Artem Dzhereleiko 2025-03-07 11:09:39 +02:00
parent 06b5706d87
commit bbedb8797b
9 changed files with 260 additions and 16 deletions

View File

@ -88,12 +88,74 @@
</mat-icon> </mat-icon>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="tb-form-row space-between"> @if (widgetActionFormGroup.get('actionSourceId').value === 'headerButton') {
<div>{{'widget-config.icon' | translate}}</div> <div class="tb-form-panel stroked">
<tb-material-icon-select asBoxInput <div class="tb-form-panel-title" translate>widget-config.header-button.button-settings</div>
formControlName="icon"> <div class="tb-form-row">
</tb-material-icon-select> <div class="fixed-title-width" translate>widget-config.header-button.button-type</div>
</div> <mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-select required formControlName="buttonType" placeholder="{{ 'widget-config.set' | translate }}">
<mat-option *ngFor="let button of widgetHeaderActionButtonTypes" [value]="button">
{{ widgetHeaderActionButtonTypeTranslationMap.get(widgetHeaderActionButtonType[button]) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-row space-between">
<div class="flex flex-row items-center justify-start gap-2">
<mat-slide-toggle class="mat-slide" formControlName="showIcon">
</mat-slide-toggle>
<div translate>widget-config.icon</div>
</div>
<tb-material-icon-select asBoxInput
formControlName="icon">
</tb-material-icon-select>
</div>
<div class="tb-form-row space-between column-xs">
<div class="fixed-title-width">{{ 'widget-config.header-button.colors' | translate }}</div>
<div class="flex flex-row items-center justify-start gap-2">
<div class="tb-small-label" translate>widget-config.header-button.color</div>
<tb-color-input asBoxInput
formControlName="buttonColor">
</tb-color-input>
<mat-divider vertical></mat-divider>
<div class="tb-small-label" translate>widget-config.header-button.background</div>
<tb-color-input asBoxInput
formControlName="buttonFillColor">
</tb-color-input>
<mat-divider vertical></mat-divider>
<div class="tb-small-label" translate>widget-config.header-button.border</div>
<tb-color-input asBoxInput
formControlName="buttonBorderColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-panel stroked">
<mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header>
<mat-panel-description class="flex items-stretch justify-start" translate>
widget-config.header-button.advanced-button-style
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-json-object-edit
[editorStyle]="{minHeight: '100px'}"
label="{{ 'widget-config.header-button.button-style' | translate }}"
formControlName="customButtonStyle"
></tb-json-object-edit>
</ng-template>
</mat-expansion-panel>
</div>
</div>
} @else {
<div class="tb-form-row space-between">
<div>{{'widget-config.icon' | translate}}</div>
<tb-material-icon-select asBoxInput
formControlName="icon">
</tb-material-icon-select>
</div>
}
<div class="tb-form-panel stroked tb-slide-toggle" *ngIf="displayShowWidgetActionForm()"> <div class="tb-form-panel stroked tb-slide-toggle" *ngIf="displayShowWidgetActionForm()">
<mat-expansion-panel class="tb-settings" [expanded]="widgetActionFormGroup.get('useShowWidgetActionFunction').value" <mat-expansion-panel class="tb-settings" [expanded]="widgetActionFormGroup.get('useShowWidgetActionFunction').value"
[disabled]="!widgetActionFormGroup.get('useShowWidgetActionFunction').value"> [disabled]="!widgetActionFormGroup.get('useShowWidgetActionFunction').value">

View File

@ -43,6 +43,10 @@ import {
CellClickColumnInfo, CellClickColumnInfo,
defaultWidgetAction, defaultWidgetAction,
WidgetActionSource, WidgetActionSource,
WidgetHeaderActionButtonType,
widgetHeaderActionButtonTypeMap,
WidgetHeaderActionButtonTypes,
widgetHeaderActionButtonTypeTranslationMap,
widgetType widgetType
} from '@shared/models/widget.models'; } from '@shared/models/widget.models';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
@ -85,6 +89,11 @@ export class WidgetActionDialogComponent extends DialogComponent<WidgetActionDia
configuredColumns: Array<CellClickColumnInfo> = []; configuredColumns: Array<CellClickColumnInfo> = [];
usedCellClickColumns: Array<number> = []; usedCellClickColumns: Array<number> = [];
widgetHeaderActionButtonType = WidgetHeaderActionButtonType
widgetHeaderActionButtonTypes = WidgetHeaderActionButtonTypes;
widgetHeaderActionButtonTypeTranslationMap = widgetHeaderActionButtonTypeTranslationMap;
widgetHeaderActionButtonTypeMap = widgetHeaderActionButtonTypeMap;
@ViewChild('columnIndexSelect') columnIndexSelect: MatSelect; @ViewChild('columnIndexSelect') columnIndexSelect: MatSelect;
columnIndexPlaceholderText = this.translate.instant('widget-config.select-column-index'); columnIndexPlaceholderText = this.translate.instant('widget-config.select-column-index');
@ -120,12 +129,19 @@ export class WidgetActionDialogComponent extends DialogComponent<WidgetActionDia
actionSourceId: [this.action.actionSourceId, Validators.required], actionSourceId: [this.action.actionSourceId, Validators.required],
columnIndex: [{value: this.checkColumnIndex(this.action.columnIndex), disabled: true}, Validators.required], columnIndex: [{value: this.checkColumnIndex(this.action.columnIndex), disabled: true}, Validators.required],
name: [this.action.name, [this.validateActionName(), Validators.required]], name: [this.action.name, [this.validateActionName(), Validators.required]],
buttonType: [isDefinedAndNotNull(this.action.buttonType) ? this.action.buttonType : WidgetHeaderActionButtonType.icon, []],
showIcon: [isDefinedAndNotNull(this.action.showIcon) ? this.action.showIcon : true, []],
icon: [this.action.icon, Validators.required], icon: [this.action.icon, Validators.required],
buttonColor: [isDefinedAndNotNull(this.action.buttonColor) ? this.action.buttonColor : 'rgba(0, 0, 0, 0.87)', []],
buttonFillColor: [isDefinedAndNotNull(this.action.buttonFillColor) ? this.action.buttonFillColor : '#3F52DD', []],
buttonBorderColor: [isDefinedAndNotNull(this.action.buttonBorderColor) ? this.action.buttonBorderColor : '#3F52DD', []],
customButtonStyle: [isDefinedAndNotNull(this.action.customButtonStyle) ? this.action.customButtonStyle : '', []],
useShowWidgetActionFunction: [this.action.useShowWidgetActionFunction], useShowWidgetActionFunction: [this.action.useShowWidgetActionFunction],
showWidgetActionFunction: [this.action.showWidgetActionFunction || 'return true;'], showWidgetActionFunction: [this.action.showWidgetActionFunction || 'return true;'],
widgetAction: [actionDescriptorToAction(toWidgetActionDescriptor(this.action)), Validators.required] widgetAction: [actionDescriptorToAction(toWidgetActionDescriptor(this.action)), Validators.required]
}); });
this.updateShowWidgetActionForm(); this.updateShowWidgetActionForm();
this.widgetHeaderButtonValidators();
this.widgetActionFormGroup.get('actionSourceId').valueChanges.pipe( this.widgetActionFormGroup.get('actionSourceId').valueChanges.pipe(
takeUntil(this.destroy$) takeUntil(this.destroy$)
).subscribe((value) => { ).subscribe((value) => {
@ -143,6 +159,9 @@ export class WidgetActionDialogComponent extends DialogComponent<WidgetActionDia
).subscribe(() => { ).subscribe(() => {
this.updateShowWidgetActionForm(); this.updateShowWidgetActionForm();
}); });
this.widgetActionFormGroup.get('buttonType').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => this.widgetHeaderButtonValidators());
setTimeout(() => { setTimeout(() => {
if (this.action?.actionSourceId === 'cellClick') { if (this.action?.actionSourceId === 'cellClick') {
this.widgetActionFormGroup.get('columnIndex').enable(); this.widgetActionFormGroup.get('columnIndex').enable();
@ -160,6 +179,31 @@ export class WidgetActionDialogComponent extends DialogComponent<WidgetActionDia
super.ngOnDestroy(); super.ngOnDestroy();
} }
widgetHeaderButtonValidators() {
const buttonType = this.widgetActionFormGroup.get('buttonType').value;
this.widgetActionFormGroup.get('showIcon').disable({emitEvent: false});
this.widgetActionFormGroup.get('buttonFillColor').disable({emitEvent: false});
this.widgetActionFormGroup.get('buttonBorderColor').disable({emitEvent: false});
switch (buttonType) {
case WidgetHeaderActionButtonType.basic:
this.widgetActionFormGroup.get('showIcon').enable({emitEvent: false});
break;
case WidgetHeaderActionButtonType.raised:
this.widgetActionFormGroup.get('showIcon').enable({emitEvent: false});
this.widgetActionFormGroup.get('buttonFillColor').enable({emitEvent: false});
break;
case WidgetHeaderActionButtonType.stroked:
case WidgetHeaderActionButtonType.flat:
this.widgetActionFormGroup.get('showIcon').enable({emitEvent: false});
this.widgetActionFormGroup.get('buttonFillColor').enable({emitEvent: false});
this.widgetActionFormGroup.get('buttonBorderColor').enable({emitEvent: false});
break;
case WidgetHeaderActionButtonType.miniFab:
this.widgetActionFormGroup.get('buttonFillColor').enable({emitEvent: false});
break;
}
}
displayShowWidgetActionForm(): boolean { displayShowWidgetActionForm(): boolean {
return !!this.data.actionsData.actionSources[this.widgetActionFormGroup.get('actionSourceId').value]?.hasShowCondition; return !!this.data.actionsData.actionSources[this.widgetActionFormGroup.get('actionSourceId').value]?.hasShowCondition;
} }

View File

@ -37,14 +37,79 @@
class="tb-widget-actions" class="tb-widget-actions"
[class]="{'tb-widget-actions-absolute': !(widget.showWidgetTitlePanel && !widgetComponent.widgetContext?.embedTitlePanel && (widget.showTitle||widget.hasAggregation))}" [class]="{'tb-widget-actions-absolute': !(widget.showWidgetTitlePanel && !widgetComponent.widgetContext?.embedTitlePanel && (widget.showTitle||widget.hasAggregation))}"
(mousedown)="$event.stopPropagation()"> (mousedown)="$event.stopPropagation()">
<button mat-icon-button *ngFor="let action of widget.customHeaderActions" <div class="flex items-center gap-2">
type="button" <ng-container *ngFor="let action of widget.customHeaderActions">
[class.!hidden]="isEdit" @switch (action.buttonType) {
(click)="action.onAction($event)" @case (widgetHeaderActionButtonType.icon) {
matTooltip="{{ action.displayName }}" <button mat-icon-button
matTooltipPosition="above"> [style]="action.customButtonStyle"
<tb-icon>{{ action.icon }}</tb-icon> [class.!hidden]="isEdit"
</button> (click)="action.onAction($event)"
matTooltip="{{ action.displayName }}"
matTooltipPosition="above">
<tb-icon [style.color]="action.buttonColor">{{ action.icon }}</tb-icon>
</button>
}
@case (widgetHeaderActionButtonType.miniFab) {
<button mat-mini-fab
[style]="action.customButtonStyle"
[class.!hidden]="isEdit"
[style.background-color]="action.buttonFillColor"
(click)="action.onAction($event)"
matTooltip="{{ action.displayName }}"
matTooltipPosition="above">
<tb-icon [style.color]="action.buttonColor">{{ action.icon }}</tb-icon>
</button>
}
@case (widgetHeaderActionButtonType.basic) {
<button [class.!hidden]="isEdit" mat-button
[style]="action.customButtonStyle"
(click)="action.onAction($event)"
matTooltip="{{ action.displayName }}"
matTooltipPosition="above">
<tb-icon matButtonIcon *ngIf="action.showIcon" [style.color]="action.buttonColor">{{ action.icon }}</tb-icon>
<span [style.color]="action.buttonColor">{{ action.displayName }}</span>
</button>
}
@case (widgetHeaderActionButtonType.raised) {
<button [class.!hidden]="isEdit" mat-raised-button
[style]="action.customButtonStyle"
[style.background-color]="action.buttonFillColor"
(click)="action.onAction($event)"
matTooltip="{{ action.displayName }}"
matTooltipPosition="above">
<tb-icon matButtonIcon *ngIf="action.showIcon" [style.color]="action.buttonColor">{{ action.icon }}</tb-icon>
<span [style.color]="action.buttonColor">{{ action.displayName }}</span>
</button>
}
@case (widgetHeaderActionButtonType.stroked) {
<button [class.!hidden]="isEdit" mat-stroked-button
[style]="action.customButtonStyle"
[style.border-color]="action.buttonBorderColor"
[style.background-color]="action.buttonFillColor"
(click)="action.onAction($event)"
matTooltip="{{ action.displayName }}"
matTooltipPosition="above">
<tb-icon matButtonIcon *ngIf="action.showIcon" [style.color]="action.buttonColor">{{ action.icon }}</tb-icon>
<span [style.color]="action.buttonColor">{{ action.displayName }}</span>
</button>
}
@case (widgetHeaderActionButtonType.flat) {
<button [class.!hidden]="isEdit" mat-flat-button
[style]="action.customButtonStyle"
[style.background-color]="action.buttonFillColor"
[style.border-color]="action.buttonBorderColor"
(click)="action.onAction($event)"
matTooltip="{{ action.displayName }}"
matTooltipPosition="above">
<tb-icon matButtonIcon *ngIf="action.showIcon" [style.color]="action.buttonColor">{{ action.icon }}</tb-icon>
<span [style.color]="action.buttonColor">{{ action.displayName }}</span>
</button>
}
}
</ng-container>
</div>
<button mat-icon-button *ngFor="let action of widget.widgetActions" <button mat-icon-button *ngFor="let action of widget.widgetActions"
type="button" type="button"
[class.!hidden]="isEdit || !action.show" [class.!hidden]="isEdit || !action.show"

View File

@ -87,7 +87,7 @@ div.tb-widget {
place-content: center flex-start; place-content: center flex-start;
align-items: center; align-items: center;
z-index: 19; z-index: 19;
margin: 5px 0 0; margin: 5px 0 5px;
&-absolute { &-absolute {
position: absolute; position: absolute;

View File

@ -44,8 +44,9 @@ import { GridsterItemComponent } from 'angular-gridster2';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { from } from 'rxjs'; import { from } from 'rxjs';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance;
import { TbContextMenuEvent } from '@shared/models/jquery-event.models'; import { TbContextMenuEvent } from '@shared/models/jquery-event.models';
import { WidgetHeaderActionButtonType } from '@shared/models/widget.models';
import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance;
export enum WidgetComponentActionType { export enum WidgetComponentActionType {
MOUSE_DOWN, MOUSE_DOWN,
@ -130,6 +131,8 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
return (this.isEditActionEnabled || this.isRemoveActionEnabled || this.isExportActionEnabled) && !this.widget?.isFullscreen; return (this.isEditActionEnabled || this.isRemoveActionEnabled || this.isExportActionEnabled) && !this.widget?.isFullscreen;
} }
widgetHeaderActionButtonType = WidgetHeaderActionButtonType;
private cssClass: string; private cssClass: string;
private editWidgetActionsTooltip: ITooltipsterInstance; private editWidgetActionsTooltip: ITooltipsterInstance;

View File

@ -295,7 +295,13 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
const headerAction: WidgetHeaderAction = { const headerAction: WidgetHeaderAction = {
name: descriptor.name, name: descriptor.name,
displayName: descriptor.displayName, displayName: descriptor.displayName,
buttonType: descriptor.buttonType,
showIcon: descriptor.showIcon,
icon: descriptor.icon, icon: descriptor.icon,
buttonColor: descriptor.buttonColor,
buttonFillColor: descriptor.buttonFillColor,
buttonBorderColor: descriptor.buttonBorderColor,
customButtonStyle: descriptor.customButtonStyle,
descriptor, descriptor,
useShowWidgetHeaderActionFunction, useShowWidgetHeaderActionFunction,
showWidgetHeaderActionFunction, showWidgetHeaderActionFunction,

View File

@ -26,6 +26,7 @@ import {
WidgetActionSource, WidgetActionSource,
WidgetConfig, WidgetConfig,
WidgetControllerDescriptor, WidgetControllerDescriptor,
WidgetHeaderActionButtonType,
WidgetType, WidgetType,
widgetType, widgetType,
WidgetTypeDescriptor, WidgetTypeDescriptor,
@ -118,6 +119,12 @@ export type ShowWidgetHeaderActionFunction = (ctx: WidgetContext, data: Formatte
export interface WidgetHeaderAction extends IWidgetAction { export interface WidgetHeaderAction extends IWidgetAction {
displayName: string; displayName: string;
descriptor: WidgetActionDescriptor; descriptor: WidgetActionDescriptor;
buttonType?: WidgetHeaderActionButtonType;
showIcon?:boolean;
buttonColor?: string;
buttonFillColor?: string;
buttonBorderColor?: string;
customButtonStyle?: string;
useShowWidgetHeaderActionFunction: boolean; useShowWidgetHeaderActionFunction: boolean;
showWidgetHeaderActionFunction: CompiledTbFunction<ShowWidgetHeaderActionFunction>; showWidgetHeaderActionFunction: CompiledTbFunction<ShowWidgetHeaderActionFunction>;
} }

View File

@ -567,6 +567,35 @@ export interface LegendData {
data: Array<LegendKeyData>; data: Array<LegendKeyData>;
} }
export enum WidgetHeaderActionButtonType {
basic = 'basic',
raised = 'raised',
stroked = 'stroked',
flat = 'flat',
icon = 'icon',
miniFab = 'miniFab'
}
export const WidgetHeaderActionButtonTypes = Object.keys(WidgetHeaderActionButtonType) as WidgetHeaderActionButtonType[];
export const widgetHeaderActionButtonTypeTranslationMap = new Map<WidgetHeaderActionButtonType, string>([
[WidgetHeaderActionButtonType.basic, 'widget-config.header-button.button-type-basic'],
[WidgetHeaderActionButtonType.raised, 'widget-config.header-button.button-type-raised'],
[WidgetHeaderActionButtonType.stroked, 'widget-config.header-button.button-type-stroked'],
[WidgetHeaderActionButtonType.flat, 'widget-config.header-button.button-type-flat'],
[WidgetHeaderActionButtonType.icon, 'widget-config.header-button.button-type-icon'],
[WidgetHeaderActionButtonType.miniFab, 'widget-config.header-button.button-type-mini-fab']
]);
export const widgetHeaderActionButtonTypeMap = new Map<WidgetHeaderActionButtonType, string>([
[WidgetHeaderActionButtonType.basic, 'mat-button'],
[WidgetHeaderActionButtonType.raised, 'mat-raised-button'],
[WidgetHeaderActionButtonType.stroked, 'mat-stroked-button'],
[WidgetHeaderActionButtonType.flat, 'mat-flat-button'],
[WidgetHeaderActionButtonType.icon, 'mat-icon-button'],
[WidgetHeaderActionButtonType.miniFab, 'mat-mini-fab']
]);
export enum WidgetActionType { export enum WidgetActionType {
doNothing = 'doNothing', doNothing = 'doNothing',
openDashboardState = 'openDashboardState', openDashboardState = 'openDashboardState',
@ -718,7 +747,13 @@ export interface WidgetAction extends CustomActionDescriptor {
export interface WidgetActionDescriptor extends WidgetAction { export interface WidgetActionDescriptor extends WidgetAction {
id: string; id: string;
name: string; name: string;
buttonType?: WidgetHeaderActionButtonType;
showIcon?: boolean;
icon: string; icon: string;
buttonColor?: string;
buttonFillColor?: string;
buttonBorderColor?: string;
customButtonStyle?: string;
displayName?: string; displayName?: string;
useShowWidgetActionFunction?: boolean; useShowWidgetActionFunction?: boolean;
showWidgetActionFunction?: TbFunction; showWidgetActionFunction?: TbFunction;
@ -729,7 +764,13 @@ export const actionDescriptorToAction = (descriptor: WidgetActionDescriptor): Wi
const result: WidgetActionDescriptor = {...descriptor}; const result: WidgetActionDescriptor = {...descriptor};
delete result.id; delete result.id;
delete result.name; delete result.name;
delete result.buttonType;
delete result.showIcon;
delete result.icon; delete result.icon;
delete result.buttonColor;
delete result.buttonFillColor;
delete result.buttonBorderColor;
delete result.customButtonStyle;
delete result.displayName; delete result.displayName;
delete result.useShowWidgetActionFunction; delete result.useShowWidgetActionFunction;
delete result.showWidgetActionFunction; delete result.showWidgetActionFunction;

View File

@ -6537,6 +6537,22 @@
"action-name-required": "Action name is required.", "action-name-required": "Action name is required.",
"action-name-not-unique": "Another action with the same name already exists.\nAction name should be unique within the same action source.", "action-name-not-unique": "Another action with the same name already exists.\nAction name should be unique within the same action source.",
"action-icon": "Icon", "action-icon": "Icon",
"header-button": {
"button-settings": "Button settings",
"button-type": "Button type",
"button-type-basic": "Basic",
"button-type-raised": "Raised",
"button-type-stroked": "Stroked",
"button-type-flat": "Flat",
"button-type-icon": "Icon",
"button-type-mini-fab": "FAB",
"colors": "Colors",
"color": "Color",
"background": "Background",
"border": "Border",
"advanced-button-style": "Advanced button style",
"button-style": "Button style"
},
"show-hide-action-using-function": "Show/hide action using function", "show-hide-action-using-function": "Show/hide action using function",
"show-action-function": "Show action function", "show-action-function": "Show action function",
"action-type": "Type", "action-type": "Type",