Commit 23121c5b5af87423c66313f072dc668c34e24faf
1 parent
3cdeb237
Conditionally show widget header actions
Showing
9 changed files
with
172 additions
and
6 deletions
@@ -54,6 +54,17 @@ | @@ -54,6 +54,17 @@ | ||
54 | <tb-material-icon-select | 54 | <tb-material-icon-select |
55 | formControlName="icon"> | 55 | formControlName="icon"> |
56 | </tb-material-icon-select> | 56 | </tb-material-icon-select> |
57 | + <mat-checkbox *ngIf="displayShowWidgetActionForm()" formControlName="useShowWidgetActionFunction" style="padding-bottom: 16px;"> | ||
58 | + {{ 'widget-config.use-show-widget-action-function' | translate }} | ||
59 | + </mat-checkbox> | ||
60 | + <tb-js-func *ngIf="displayShowWidgetActionForm() && widgetActionFormGroup.get('useShowWidgetActionFunction').value" | ||
61 | + formControlName="showWidgetActionFunction" | ||
62 | + [functionArgs]="['widgetContext', 'data']" | ||
63 | + [globalVariables]="functionScopeVariables" | ||
64 | + [validationArgs]="[]" | ||
65 | + [resultType]="'boolean'" | ||
66 | + [editorCompleter]="customActionEditorCompleter" | ||
67 | + ></tb-js-func> | ||
57 | <mat-form-field class="mat-block"> | 68 | <mat-form-field class="mat-block"> |
58 | <mat-label translate>widget-config.action-type</mat-label> | 69 | <mat-label translate>widget-config.action-type</mat-label> |
59 | <mat-select required formControlName="type"> | 70 | <mat-select required formControlName="type"> |
@@ -38,7 +38,12 @@ import { | @@ -38,7 +38,12 @@ import { | ||
38 | WidgetActionsData | 38 | WidgetActionsData |
39 | } from '@home/components/widget/action/manage-widget-actions.component.models'; | 39 | } from '@home/components/widget/action/manage-widget-actions.component.models'; |
40 | import { UtilsService } from '@core/services/utils.service'; | 40 | import { UtilsService } from '@core/services/utils.service'; |
41 | -import { WidgetActionSource, WidgetActionType, widgetActionTypeTranslationMap } from '@shared/models/widget.models'; | 41 | +import { |
42 | + WidgetActionSource, | ||
43 | + widgetActionSources, | ||
44 | + WidgetActionType, | ||
45 | + widgetActionTypeTranslationMap | ||
46 | +} from '@shared/models/widget.models'; | ||
42 | import { map, mergeMap, startWith, tap } from 'rxjs/operators'; | 47 | import { map, mergeMap, startWith, tap } from 'rxjs/operators'; |
43 | import { DashboardService } from '@core/http/dashboard.service'; | 48 | import { DashboardService } from '@core/http/dashboard.service'; |
44 | import { Dashboard } from '@shared/models/dashboard.models'; | 49 | import { Dashboard } from '@shared/models/dashboard.models'; |
@@ -124,17 +129,41 @@ export class WidgetActionDialogComponent extends DialogComponent<WidgetActionDia | @@ -124,17 +129,41 @@ export class WidgetActionDialogComponent extends DialogComponent<WidgetActionDia | ||
124 | this.fb.control(this.action.name, [this.validateActionName(), Validators.required])); | 129 | this.fb.control(this.action.name, [this.validateActionName(), Validators.required])); |
125 | this.widgetActionFormGroup.addControl('icon', | 130 | this.widgetActionFormGroup.addControl('icon', |
126 | this.fb.control(this.action.icon, [Validators.required])); | 131 | this.fb.control(this.action.icon, [Validators.required])); |
132 | + this.widgetActionFormGroup.addControl('useShowWidgetActionFunction', | ||
133 | + this.fb.control(this.action.useShowWidgetActionFunction, [])); | ||
134 | + this.widgetActionFormGroup.addControl('showWidgetActionFunction', | ||
135 | + this.fb.control(this.action.showWidgetActionFunction || 'return true;', [])); | ||
127 | this.widgetActionFormGroup.addControl('type', | 136 | this.widgetActionFormGroup.addControl('type', |
128 | this.fb.control(this.action.type, [Validators.required])); | 137 | this.fb.control(this.action.type, [Validators.required])); |
138 | + this.updateShowWidgetActionForm(); | ||
129 | this.updateActionTypeFormGroup(this.action.type, this.action); | 139 | this.updateActionTypeFormGroup(this.action.type, this.action); |
130 | this.widgetActionFormGroup.get('type').valueChanges.subscribe((type: WidgetActionType) => { | 140 | this.widgetActionFormGroup.get('type').valueChanges.subscribe((type: WidgetActionType) => { |
131 | this.updateActionTypeFormGroup(type); | 141 | this.updateActionTypeFormGroup(type); |
132 | }); | 142 | }); |
133 | this.widgetActionFormGroup.get('actionSourceId').valueChanges.subscribe(() => { | 143 | this.widgetActionFormGroup.get('actionSourceId').valueChanges.subscribe(() => { |
134 | this.widgetActionFormGroup.get('name').updateValueAndValidity(); | 144 | this.widgetActionFormGroup.get('name').updateValueAndValidity(); |
145 | + this.updateShowWidgetActionForm(); | ||
146 | + }); | ||
147 | + this.widgetActionFormGroup.get('useShowWidgetActionFunction').valueChanges.subscribe(() => { | ||
148 | + this.updateShowWidgetActionForm(); | ||
135 | }); | 149 | }); |
136 | } | 150 | } |
137 | 151 | ||
152 | + displayShowWidgetActionForm(): boolean { | ||
153 | + return this.widgetActionFormGroup.get('actionSourceId').value === widgetActionSources.headerButton.value; | ||
154 | + } | ||
155 | + | ||
156 | + private updateShowWidgetActionForm() { | ||
157 | + const actionSourceId = this.widgetActionFormGroup.get('actionSourceId').value; | ||
158 | + const useShowWidgetActionFunction = this.widgetActionFormGroup.get('useShowWidgetActionFunction').value; | ||
159 | + if (actionSourceId === widgetActionSources.headerButton.value && useShowWidgetActionFunction) { | ||
160 | + this.widgetActionFormGroup.get('showWidgetActionFunction').setValidators([Validators.required]); | ||
161 | + } else { | ||
162 | + this.widgetActionFormGroup.get('showWidgetActionFunction').clearValidators(); | ||
163 | + } | ||
164 | + this.widgetActionFormGroup.get('showWidgetActionFunction').updateValueAndValidity(); | ||
165 | + } | ||
166 | + | ||
138 | private updateActionTypeFormGroup(type?: WidgetActionType, action?: WidgetActionDescriptorInfo) { | 167 | private updateActionTypeFormGroup(type?: WidgetActionType, action?: WidgetActionDescriptorInfo) { |
139 | this.actionTypeFormGroup = this.fb.group({}); | 168 | this.actionTypeFormGroup = this.fb.group({}); |
140 | if (type) { | 169 | if (type) { |
@@ -330,7 +330,7 @@ export function parseData(input: DatasourceData[]): FormattedData[] { | @@ -330,7 +330,7 @@ export function parseData(input: DatasourceData[]): FormattedData[] { | ||
330 | deviceType: null | 330 | deviceType: null |
331 | }; | 331 | }; |
332 | entityArray.filter(el => el.data.length).forEach(el => { | 332 | entityArray.filter(el => el.data.length).forEach(el => { |
333 | - const indexDate = el.data.length ? el.data.length - 1 : 0; | 333 | + const indexDate = el.data.length - 1; |
334 | if (!obj.hasOwnProperty(el.dataKey.label) || el.data[indexDate][1] !== '') { | 334 | if (!obj.hasOwnProperty(el.dataKey.label) || el.data[indexDate][1] !== '') { |
335 | obj[el.dataKey.label] = el.data[indexDate][1]; | 335 | obj[el.dataKey.label] = el.data[indexDate][1]; |
336 | obj[el.dataKey.label + '|ts'] = el.data[indexDate][0]; | 336 | obj[el.dataKey.label + '|ts'] = el.data[indexDate][0]; |
@@ -55,9 +55,17 @@ import { AppState } from '@core/core.state'; | @@ -55,9 +55,17 @@ import { AppState } from '@core/core.state'; | ||
55 | import { WidgetService } from '@core/http/widget.service'; | 55 | import { WidgetService } from '@core/http/widget.service'; |
56 | import { UtilsService } from '@core/services/utils.service'; | 56 | import { UtilsService } from '@core/services/utils.service'; |
57 | import { forkJoin, Observable, of, ReplaySubject, Subscription, throwError } from 'rxjs'; | 57 | import { forkJoin, Observable, of, ReplaySubject, Subscription, throwError } from 'rxjs'; |
58 | -import { deepClone, insertVariable, isDefined, objToBase64, objToBase64URI, validateEntityId } from '@core/utils'; | ||
59 | import { | 58 | import { |
60 | - IDynamicWidgetComponent, | 59 | + deepClone, |
60 | + insertVariable, | ||
61 | + isDefined, | ||
62 | + isNotEmptyStr, | ||
63 | + objToBase64, | ||
64 | + objToBase64URI, | ||
65 | + validateEntityId | ||
66 | +} from '@core/utils'; | ||
67 | +import { | ||
68 | + IDynamicWidgetComponent, ShowWidgetHeaderActionFunction, | ||
61 | WidgetContext, | 69 | WidgetContext, |
62 | WidgetHeaderAction, | 70 | WidgetHeaderAction, |
63 | WidgetInfo, | 71 | WidgetInfo, |
@@ -290,12 +298,25 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | @@ -290,12 +298,25 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | ||
290 | 298 | ||
291 | this.widgetContext.customHeaderActions = []; | 299 | this.widgetContext.customHeaderActions = []; |
292 | const headerActionsDescriptors = this.getActionDescriptors(widgetActionSources.headerButton.value); | 300 | const headerActionsDescriptors = this.getActionDescriptors(widgetActionSources.headerButton.value); |
293 | - headerActionsDescriptors.forEach((descriptor) => { | 301 | + headerActionsDescriptors.forEach((descriptor) => |
302 | + { | ||
303 | + let useShowWidgetHeaderActionFunction = descriptor.useShowWidgetActionFunction || false; | ||
304 | + let showWidgetHeaderActionFunction: ShowWidgetHeaderActionFunction = null; | ||
305 | + if (useShowWidgetHeaderActionFunction && isNotEmptyStr(descriptor.showWidgetActionFunction)) { | ||
306 | + try { | ||
307 | + showWidgetHeaderActionFunction = | ||
308 | + new Function('widgetContext', 'data', descriptor.showWidgetActionFunction) as ShowWidgetHeaderActionFunction; | ||
309 | + } catch (e) { | ||
310 | + useShowWidgetHeaderActionFunction = false; | ||
311 | + } | ||
312 | + } | ||
294 | const headerAction: WidgetHeaderAction = { | 313 | const headerAction: WidgetHeaderAction = { |
295 | name: descriptor.name, | 314 | name: descriptor.name, |
296 | displayName: descriptor.displayName, | 315 | displayName: descriptor.displayName, |
297 | icon: descriptor.icon, | 316 | icon: descriptor.icon, |
298 | descriptor, | 317 | descriptor, |
318 | + useShowWidgetHeaderActionFunction, | ||
319 | + showWidgetHeaderActionFunction, | ||
299 | onAction: $event => { | 320 | onAction: $event => { |
300 | const entityInfo = this.getActiveEntityInfo(); | 321 | const entityInfo = this.getActiveEntityInfo(); |
301 | const entityId = entityInfo ? entityInfo.entityId : null; | 322 | const entityId = entityInfo ? entityInfo.entityId : null; |
@@ -507,6 +528,9 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | @@ -507,6 +528,9 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | ||
507 | this.widgetInstanceInited = true; | 528 | this.widgetInstanceInited = true; |
508 | if (this.dataUpdatePending) { | 529 | if (this.dataUpdatePending) { |
509 | this.widgetTypeInstance.onDataUpdated(); | 530 | this.widgetTypeInstance.onDataUpdated(); |
531 | + setTimeout(() => { | ||
532 | + this.dashboardWidget.updateCustomHeaderActions(true); | ||
533 | + }, 0); | ||
510 | this.dataUpdatePending = false; | 534 | this.dataUpdatePending = false; |
511 | } | 535 | } |
512 | if (this.pendingMessage) { | 536 | if (this.pendingMessage) { |
@@ -844,6 +868,9 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | @@ -844,6 +868,9 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | ||
844 | if (this.displayWidgetInstance()) { | 868 | if (this.displayWidgetInstance()) { |
845 | if (this.widgetInstanceInited) { | 869 | if (this.widgetInstanceInited) { |
846 | this.widgetTypeInstance.onDataUpdated(); | 870 | this.widgetTypeInstance.onDataUpdated(); |
871 | + setTimeout(() => { | ||
872 | + this.dashboardWidget.updateCustomHeaderActions(true); | ||
873 | + }, 0); | ||
847 | } else { | 874 | } else { |
848 | this.dataUpdatePending = true; | 875 | this.dataUpdatePending = true; |
849 | } | 876 | } |
@@ -25,6 +25,8 @@ import { IterableDiffer, KeyValueDiffer } from '@angular/core'; | @@ -25,6 +25,8 @@ import { IterableDiffer, KeyValueDiffer } from '@angular/core'; | ||
25 | import { IAliasController, IStateController } from '@app/core/api/widget-api.models'; | 25 | import { IAliasController, IStateController } from '@app/core/api/widget-api.models'; |
26 | import { enumerable } from '@shared/decorators/enumerable'; | 26 | import { enumerable } from '@shared/decorators/enumerable'; |
27 | import { UtilsService } from '@core/services/utils.service'; | 27 | import { UtilsService } from '@core/services/utils.service'; |
28 | +import { FormattedData } from '@home/components/widget/lib/maps/map-models'; | ||
29 | +import { parseData } from '@home/components/widget/lib/maps/common-maps-utils'; | ||
28 | 30 | ||
29 | export interface WidgetsData { | 31 | export interface WidgetsData { |
30 | widgets: Array<Widget>; | 32 | widgets: Array<Widget>; |
@@ -438,13 +440,45 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { | @@ -438,13 +440,45 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { | ||
438 | 440 | ||
439 | this.showWidgetActions = !this.widgetContext.hideTitlePanel; | 441 | this.showWidgetActions = !this.widgetContext.hideTitlePanel; |
440 | 442 | ||
441 | - this.customHeaderActions = this.widgetContext.customHeaderActions ? this.widgetContext.customHeaderActions : []; | 443 | + this.updateCustomHeaderActions(); |
442 | this.widgetActions = this.widgetContext.widgetActions ? this.widgetContext.widgetActions : []; | 444 | this.widgetActions = this.widgetContext.widgetActions ? this.widgetContext.widgetActions : []; |
443 | if (detectChanges) { | 445 | if (detectChanges) { |
444 | this.widgetContext.detectContainerChanges(); | 446 | this.widgetContext.detectContainerChanges(); |
445 | } | 447 | } |
446 | } | 448 | } |
447 | 449 | ||
450 | + updateCustomHeaderActions(detectChanges = false) { | ||
451 | + let customHeaderActions: Array<WidgetHeaderAction>; | ||
452 | + if (this.widgetContext.customHeaderActions) { | ||
453 | + let data: FormattedData[] = []; | ||
454 | + if (this.widgetContext.customHeaderActions.some(action => action.useShowWidgetHeaderActionFunction)) { | ||
455 | + data = parseData(this.widgetContext.data || []); | ||
456 | + } | ||
457 | + customHeaderActions = this.widgetContext.customHeaderActions.filter(action => this.filterCustomHeaderAction(action, data)); | ||
458 | + } else { | ||
459 | + customHeaderActions = []; | ||
460 | + } | ||
461 | + if (!isEqual(this.customHeaderActions, customHeaderActions)) { | ||
462 | + this.customHeaderActions = customHeaderActions; | ||
463 | + if (detectChanges) { | ||
464 | + this.widgetContext.detectContainerChanges(); | ||
465 | + } | ||
466 | + } | ||
467 | + } | ||
468 | + | ||
469 | + private filterCustomHeaderAction(action: WidgetHeaderAction, data: FormattedData[]): boolean { | ||
470 | + if (action.useShowWidgetHeaderActionFunction) { | ||
471 | + try { | ||
472 | + return action.showWidgetHeaderActionFunction(this.widgetContext, data); | ||
473 | + } catch (e) { | ||
474 | + console.warn('Failed to execute showWidgetHeaderActionFunction', e); | ||
475 | + return false; | ||
476 | + } | ||
477 | + } else { | ||
478 | + return true; | ||
479 | + } | ||
480 | + } | ||
481 | + | ||
448 | @enumerable(true) | 482 | @enumerable(true) |
449 | get x(): number { | 483 | get x(): number { |
450 | let res; | 484 | let res; |
@@ -79,6 +79,7 @@ import { SortOrder } from '@shared/models/page/sort-order'; | @@ -79,6 +79,7 @@ import { SortOrder } from '@shared/models/page/sort-order'; | ||
79 | import { DomSanitizer } from '@angular/platform-browser'; | 79 | import { DomSanitizer } from '@angular/platform-browser'; |
80 | import { Router } from '@angular/router'; | 80 | import { Router } from '@angular/router'; |
81 | import { catchError, map, mergeMap, switchMap } from 'rxjs/operators'; | 81 | import { catchError, map, mergeMap, switchMap } from 'rxjs/operators'; |
82 | +import { FormattedData } from '@home/components/widget/lib/maps/map-models'; | ||
82 | 83 | ||
83 | export interface IWidgetAction { | 84 | export interface IWidgetAction { |
84 | name: string; | 85 | name: string; |
@@ -86,9 +87,13 @@ export interface IWidgetAction { | @@ -86,9 +87,13 @@ export interface IWidgetAction { | ||
86 | onAction: ($event: Event) => void; | 87 | onAction: ($event: Event) => void; |
87 | } | 88 | } |
88 | 89 | ||
90 | +export type ShowWidgetHeaderActionFunction = (ctx: WidgetContext, data: FormattedData[]) => boolean; | ||
91 | + | ||
89 | export interface WidgetHeaderAction extends IWidgetAction { | 92 | export interface WidgetHeaderAction extends IWidgetAction { |
90 | displayName: string; | 93 | displayName: string; |
91 | descriptor: WidgetActionDescriptor; | 94 | descriptor: WidgetActionDescriptor; |
95 | + useShowWidgetHeaderActionFunction: boolean; | ||
96 | + showWidgetHeaderActionFunction: ShowWidgetHeaderActionFunction; | ||
92 | } | 97 | } |
93 | 98 | ||
94 | export interface WidgetAction extends IWidgetAction { | 99 | export interface WidgetAction extends IWidgetAction { |
@@ -390,6 +390,63 @@ export const widgetContextCompletions: TbEditorCompletions = { | @@ -390,6 +390,63 @@ export const widgetContextCompletions: TbEditorCompletions = { | ||
390 | meta: 'property', | 390 | meta: 'property', |
391 | type: 'number' | 391 | type: 'number' |
392 | }, | 392 | }, |
393 | + currentUser: { | ||
394 | + description: 'Current user object.', | ||
395 | + meta: 'property', | ||
396 | + type: '<a href="https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/shared/models/user.model.ts#L45">AuthUser</a>', | ||
397 | + children: { | ||
398 | + sub: { | ||
399 | + description: 'User subject (email).', | ||
400 | + meta: 'property', | ||
401 | + type: 'string' | ||
402 | + }, | ||
403 | + scopes: { | ||
404 | + description: 'User security scopes.', | ||
405 | + meta: 'property', | ||
406 | + type: 'Array<string>' | ||
407 | + }, | ||
408 | + userId: { | ||
409 | + description: 'User id.', | ||
410 | + meta: 'property', | ||
411 | + type: 'string' | ||
412 | + }, | ||
413 | + firstName: { | ||
414 | + description: 'User first name.', | ||
415 | + meta: 'property', | ||
416 | + type: 'string' | ||
417 | + }, | ||
418 | + lastName: { | ||
419 | + description: 'User last name.', | ||
420 | + meta: 'property', | ||
421 | + type: 'string' | ||
422 | + }, | ||
423 | + enabled: { | ||
424 | + description: 'Whether is user enabled.', | ||
425 | + meta: 'property', | ||
426 | + type: 'boolean' | ||
427 | + }, | ||
428 | + tenantId: { | ||
429 | + description: 'Tenant id of the user.', | ||
430 | + meta: 'property', | ||
431 | + type: 'string' | ||
432 | + }, | ||
433 | + customerId: { | ||
434 | + description: 'Customer id of the user (available when user belongs to specific customer).', | ||
435 | + meta: 'property', | ||
436 | + type: 'string' | ||
437 | + }, | ||
438 | + isPublic: { | ||
439 | + description: 'Special flag indicating public user.', | ||
440 | + meta: 'property', | ||
441 | + type: 'boolean' | ||
442 | + }, | ||
443 | + authority: { | ||
444 | + description: 'User authority. Possible values: SYS_ADMIN, TENANT_ADMIN, CUSTOMER_USER', | ||
445 | + meta: 'property', | ||
446 | + type: '<a href="https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/shared/models/authority.enum.ts#L17">Authority</a>' | ||
447 | + } | ||
448 | + } | ||
449 | + }, | ||
393 | hideTitlePanel: { | 450 | hideTitlePanel: { |
394 | description: 'Manages visibility of widget title panel. Useful for widget with custom title panels or different states. <b>updateWidgetParams()</b> function must be called after this property change.', | 451 | description: 'Manages visibility of widget title panel. Useful for widget with custom title panels or different states. <b>updateWidgetParams()</b> function must be called after this property change.', |
395 | meta: 'property', | 452 | meta: 'property', |
@@ -462,6 +462,8 @@ export interface WidgetActionDescriptor extends CustomActionDescriptor { | @@ -462,6 +462,8 @@ export interface WidgetActionDescriptor extends CustomActionDescriptor { | ||
462 | setEntityId?: boolean; | 462 | setEntityId?: boolean; |
463 | stateEntityParamName?: string; | 463 | stateEntityParamName?: string; |
464 | mobileAction?: WidgetMobileActionDescriptor; | 464 | mobileAction?: WidgetMobileActionDescriptor; |
465 | + useShowWidgetActionFunction?: boolean; | ||
466 | + showWidgetActionFunction?: string; | ||
465 | } | 467 | } |
466 | 468 | ||
467 | export interface WidgetComparisonSettings { | 469 | export interface WidgetComparisonSettings { |
@@ -3038,6 +3038,7 @@ | @@ -3038,6 +3038,7 @@ | ||
3038 | "action-name-required": "Action name is required.", | 3038 | "action-name-required": "Action name is required.", |
3039 | "action-name-not-unique": "Another action with the same name already exists.<br/>Action name should be unique within the same action source.", | 3039 | "action-name-not-unique": "Another action with the same name already exists.<br/>Action name should be unique within the same action source.", |
3040 | "action-icon": "Icon", | 3040 | "action-icon": "Icon", |
3041 | + "use-show-widget-action-function": "Use show widget action function", | ||
3041 | "action-type": "Type", | 3042 | "action-type": "Type", |
3042 | "action-type-required": "Action type is required.", | 3043 | "action-type-required": "Action type is required.", |
3043 | "edit-action": "Edit action", | 3044 | "edit-action": "Edit action", |