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 | 54 | <tb-material-icon-select |
55 | 55 | formControlName="icon"> |
56 | 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 | 68 | <mat-form-field class="mat-block"> |
58 | 69 | <mat-label translate>widget-config.action-type</mat-label> |
59 | 70 | <mat-select required formControlName="type"> | ... | ... |
... | ... | @@ -38,7 +38,12 @@ import { |
38 | 38 | WidgetActionsData |
39 | 39 | } from '@home/components/widget/action/manage-widget-actions.component.models'; |
40 | 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 | 47 | import { map, mergeMap, startWith, tap } from 'rxjs/operators'; |
43 | 48 | import { DashboardService } from '@core/http/dashboard.service'; |
44 | 49 | import { Dashboard } from '@shared/models/dashboard.models'; |
... | ... | @@ -124,17 +129,41 @@ export class WidgetActionDialogComponent extends DialogComponent<WidgetActionDia |
124 | 129 | this.fb.control(this.action.name, [this.validateActionName(), Validators.required])); |
125 | 130 | this.widgetActionFormGroup.addControl('icon', |
126 | 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 | 136 | this.widgetActionFormGroup.addControl('type', |
128 | 137 | this.fb.control(this.action.type, [Validators.required])); |
138 | + this.updateShowWidgetActionForm(); | |
129 | 139 | this.updateActionTypeFormGroup(this.action.type, this.action); |
130 | 140 | this.widgetActionFormGroup.get('type').valueChanges.subscribe((type: WidgetActionType) => { |
131 | 141 | this.updateActionTypeFormGroup(type); |
132 | 142 | }); |
133 | 143 | this.widgetActionFormGroup.get('actionSourceId').valueChanges.subscribe(() => { |
134 | 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 | 167 | private updateActionTypeFormGroup(type?: WidgetActionType, action?: WidgetActionDescriptorInfo) { |
139 | 168 | this.actionTypeFormGroup = this.fb.group({}); |
140 | 169 | if (type) { | ... | ... |
... | ... | @@ -330,7 +330,7 @@ export function parseData(input: DatasourceData[]): FormattedData[] { |
330 | 330 | deviceType: null |
331 | 331 | }; |
332 | 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 | 334 | if (!obj.hasOwnProperty(el.dataKey.label) || el.data[indexDate][1] !== '') { |
335 | 335 | obj[el.dataKey.label] = el.data[indexDate][1]; |
336 | 336 | obj[el.dataKey.label + '|ts'] = el.data[indexDate][0]; | ... | ... |
... | ... | @@ -55,9 +55,17 @@ import { AppState } from '@core/core.state'; |
55 | 55 | import { WidgetService } from '@core/http/widget.service'; |
56 | 56 | import { UtilsService } from '@core/services/utils.service'; |
57 | 57 | import { forkJoin, Observable, of, ReplaySubject, Subscription, throwError } from 'rxjs'; |
58 | -import { deepClone, insertVariable, isDefined, objToBase64, objToBase64URI, validateEntityId } from '@core/utils'; | |
59 | 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 | 69 | WidgetContext, |
62 | 70 | WidgetHeaderAction, |
63 | 71 | WidgetInfo, |
... | ... | @@ -290,12 +298,25 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI |
290 | 298 | |
291 | 299 | this.widgetContext.customHeaderActions = []; |
292 | 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 | 313 | const headerAction: WidgetHeaderAction = { |
295 | 314 | name: descriptor.name, |
296 | 315 | displayName: descriptor.displayName, |
297 | 316 | icon: descriptor.icon, |
298 | 317 | descriptor, |
318 | + useShowWidgetHeaderActionFunction, | |
319 | + showWidgetHeaderActionFunction, | |
299 | 320 | onAction: $event => { |
300 | 321 | const entityInfo = this.getActiveEntityInfo(); |
301 | 322 | const entityId = entityInfo ? entityInfo.entityId : null; |
... | ... | @@ -507,6 +528,9 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI |
507 | 528 | this.widgetInstanceInited = true; |
508 | 529 | if (this.dataUpdatePending) { |
509 | 530 | this.widgetTypeInstance.onDataUpdated(); |
531 | + setTimeout(() => { | |
532 | + this.dashboardWidget.updateCustomHeaderActions(true); | |
533 | + }, 0); | |
510 | 534 | this.dataUpdatePending = false; |
511 | 535 | } |
512 | 536 | if (this.pendingMessage) { |
... | ... | @@ -844,6 +868,9 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI |
844 | 868 | if (this.displayWidgetInstance()) { |
845 | 869 | if (this.widgetInstanceInited) { |
846 | 870 | this.widgetTypeInstance.onDataUpdated(); |
871 | + setTimeout(() => { | |
872 | + this.dashboardWidget.updateCustomHeaderActions(true); | |
873 | + }, 0); | |
847 | 874 | } else { |
848 | 875 | this.dataUpdatePending = true; |
849 | 876 | } | ... | ... |
... | ... | @@ -25,6 +25,8 @@ import { IterableDiffer, KeyValueDiffer } from '@angular/core'; |
25 | 25 | import { IAliasController, IStateController } from '@app/core/api/widget-api.models'; |
26 | 26 | import { enumerable } from '@shared/decorators/enumerable'; |
27 | 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 | 31 | export interface WidgetsData { |
30 | 32 | widgets: Array<Widget>; |
... | ... | @@ -438,13 +440,45 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { |
438 | 440 | |
439 | 441 | this.showWidgetActions = !this.widgetContext.hideTitlePanel; |
440 | 442 | |
441 | - this.customHeaderActions = this.widgetContext.customHeaderActions ? this.widgetContext.customHeaderActions : []; | |
443 | + this.updateCustomHeaderActions(); | |
442 | 444 | this.widgetActions = this.widgetContext.widgetActions ? this.widgetContext.widgetActions : []; |
443 | 445 | if (detectChanges) { |
444 | 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 | 482 | @enumerable(true) |
449 | 483 | get x(): number { |
450 | 484 | let res; | ... | ... |
... | ... | @@ -79,6 +79,7 @@ import { SortOrder } from '@shared/models/page/sort-order'; |
79 | 79 | import { DomSanitizer } from '@angular/platform-browser'; |
80 | 80 | import { Router } from '@angular/router'; |
81 | 81 | import { catchError, map, mergeMap, switchMap } from 'rxjs/operators'; |
82 | +import { FormattedData } from '@home/components/widget/lib/maps/map-models'; | |
82 | 83 | |
83 | 84 | export interface IWidgetAction { |
84 | 85 | name: string; |
... | ... | @@ -86,9 +87,13 @@ export interface IWidgetAction { |
86 | 87 | onAction: ($event: Event) => void; |
87 | 88 | } |
88 | 89 | |
90 | +export type ShowWidgetHeaderActionFunction = (ctx: WidgetContext, data: FormattedData[]) => boolean; | |
91 | + | |
89 | 92 | export interface WidgetHeaderAction extends IWidgetAction { |
90 | 93 | displayName: string; |
91 | 94 | descriptor: WidgetActionDescriptor; |
95 | + useShowWidgetHeaderActionFunction: boolean; | |
96 | + showWidgetHeaderActionFunction: ShowWidgetHeaderActionFunction; | |
92 | 97 | } |
93 | 98 | |
94 | 99 | export interface WidgetAction extends IWidgetAction { | ... | ... |
... | ... | @@ -390,6 +390,63 @@ export const widgetContextCompletions: TbEditorCompletions = { |
390 | 390 | meta: 'property', |
391 | 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 | 450 | hideTitlePanel: { |
394 | 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 | 452 | meta: 'property', | ... | ... |
... | ... | @@ -462,6 +462,8 @@ export interface WidgetActionDescriptor extends CustomActionDescriptor { |
462 | 462 | setEntityId?: boolean; |
463 | 463 | stateEntityParamName?: string; |
464 | 464 | mobileAction?: WidgetMobileActionDescriptor; |
465 | + useShowWidgetActionFunction?: boolean; | |
466 | + showWidgetActionFunction?: string; | |
465 | 467 | } |
466 | 468 | |
467 | 469 | export interface WidgetComparisonSettings { | ... | ... |
... | ... | @@ -3038,6 +3038,7 @@ |
3038 | 3038 | "action-name-required": "Action name is required.", |
3039 | 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 | 3040 | "action-icon": "Icon", |
3041 | + "use-show-widget-action-function": "Use show widget action function", | |
3041 | 3042 | "action-type": "Type", |
3042 | 3043 | "action-type-required": "Action type is required.", |
3043 | 3044 | "edit-action": "Edit action", | ... | ... |