From 5dcf9049a1520ac65a2605b187fdea421c95625e Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Fri, 10 Jul 2026 13:31:48 +0200 Subject: [PATCH 1/2] Added double click and right click tag action triggers for SCADA symbols --- .../widget/lib/scada/scada-symbol.models.ts | 15 +++- ...l-metadata-tag-function-panel.component.ts | 10 ++- .../scada-symbol-metadata-tag.component.html | 27 ++++--- .../scada-symbol-metadata-tag.component.scss | 9 +-- .../scada-symbol-metadata-tag.component.ts | 77 ++++++++++++------- .../scada-symbol-metadata-tags.component.html | 4 +- .../scada-symbol-metadata-tags.component.scss | 9 +-- .../scada-symbol-metadata-tags.component.ts | 31 +++++--- .../scada-symbol-metadata.component.ts | 10 ++- .../scada-symbol-editor.models.ts | 26 ++++--- .../scada-symbol-tooltip.components.ts | 63 +++++++++------ .../scada-symbol/scada-symbol.component.ts | 9 ++- .../assets/locale/locale.constant-en_US.json | 2 + 13 files changed, 181 insertions(+), 111 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts index 4c250a4618..4e6132926c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts @@ -116,7 +116,17 @@ export type ScadaSymbolStateRenderFunction = (ctx: ScadaSymbolContext, svg: Svg) export type ScadaSymbolTagStateRenderFunction = (ctx: ScadaSymbolContext, element: Element) => void; // noinspection JSUnusedGlobalSymbols -export type ScadaSymbolActionTrigger = 'click'; +export type ScadaSymbolActionTrigger = 'click' | 'dblclick' | 'contextmenu'; + +export const scadaSymbolActionTriggers: ScadaSymbolActionTrigger[] = ['click', 'dblclick', 'contextmenu']; + +export const scadaSymbolActionTriggerTranslations = new Map( + [ + ['click', 'scada.tag.on-click-action'], + ['dblclick', 'scada.tag.on-double-click-action'], + ['contextmenu', 'scada.tag.on-right-click-action'] + ] +); export type ScadaSymbolActionFunction = (ctx: ScadaSymbolContext, element: Element, event: Event) => void; export interface ScadaSymbolAction { @@ -674,6 +684,9 @@ export class ScadaSymbolObject { elements.forEach(element => { element.attr('cursor', 'pointer'); element.on(trigger, (event) => { + if (trigger === 'contextmenu') { + event.preventDefault(); + } action.action(this.context, element, event); }); }); diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component.ts index 2e1a7e4d29..10da5339b0 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component.ts @@ -34,6 +34,10 @@ import { scadaSymbolRenderFunctionHighlightRules } from '@home/pages/scada-symbol/scada-symbol-editor.models'; import { JsFuncComponent } from '@shared/components/js-func.component'; +import { + ScadaSymbolActionTrigger, + scadaSymbolActionTriggerTranslations +} from '@home/components/widget/lib/scada/scada-symbol.models'; @Component({ selector: 'tb-scada-symbol-metadata-tag-function-panel', @@ -51,7 +55,7 @@ export class ScadaSymbolMetadataTagFunctionPanelComponent implements OnInit, Aft tagFunction: string; @Input() - tagFunctionType: 'renderFunction' | 'clickAction'; + tagFunctionType: 'renderFunction' | ScadaSymbolActionTrigger; @Input() tag: string; @@ -98,8 +102,8 @@ export class ScadaSymbolMetadataTagFunctionPanelComponent implements OnInit, Aft this.tagFunctionArgs = ['ctx', 'element']; this.highlightRules = scadaSymbolRenderFunctionHighlightRules; this.tagFunctionHelpId = 'scada/tag_state_render_fn'; - } else if (this.tagFunctionType === 'clickAction') { - this.panelTitle = 'scada.tag.on-click-action'; + } else { + this.panelTitle = scadaSymbolActionTriggerTranslations.get(this.tagFunctionType); this.tagFunctionArgs = ['ctx', 'element', 'event']; this.highlightRules = scadaSymbolClickActionHighlightRules; this.tagFunctionHelpId = 'scada/tag_click_action_fn'; diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.html index b3a64d6c0e..fd8dfae708 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.html +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.html @@ -37,16 +37,19 @@ } -
- @if (!disabled || tagFormGroup.get('clickAction').value) { - - } -
+ @for (trigger of actionTriggers; track trigger) { +
+ @if (!disabled || tagFormGroup.get(actionControlName(trigger)).value) { + + } +
+ } diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.scss b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.scss index 88a0cd4d20..28958522ac 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.scss +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.scss @@ -15,15 +15,10 @@ */ .tb-scada-symbol-metadata-tag-row { .tb-tag-field { - flex: 1 1 70%; + flex: 1 1 40%; } - .tb-state-render-function-field { + .tb-state-render-function-field, .tb-action-function-field { flex: 1 1 15%; - } - .tb-on-click-action-field { - flex: 1 1 15%; - } - .tb-state-render-function-field, .tb-on-click-action-field { display: flex; justify-content: flex-end; .mdc-button { diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.ts index a1237d87dc..35b9c395b1 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.ts @@ -19,8 +19,10 @@ import { forwardRef, Input, OnInit, + QueryList, Renderer2, ViewChild, + ViewChildren, ViewContainerRef, ViewEncapsulation } from '@angular/core'; @@ -34,7 +36,11 @@ import { UntypedFormGroup, Validator } from '@angular/forms'; -import { ScadaSymbolTag } from '@home/components/widget/lib/scada/scada-symbol.models'; +import { + ScadaSymbolActionTrigger, + scadaSymbolActionTriggers, + ScadaSymbolTag +} from '@home/components/widget/lib/scada/scada-symbol.models'; import { TbEditorCompleter } from '@shared/models/ace/completion.models'; import { MatButton } from '@angular/material/button'; import { TbPopoverService } from '@shared/components/popover.service'; @@ -66,8 +72,8 @@ export class ScadaSymbolMetadataTagComponent implements ControlValueAccessor, On @ViewChild('editStateRenderFunctionButton') editStateRenderFunctionButton: MatButton; - @ViewChild('editClickActionButton') - editClickActionButton: MatButton; + @ViewChildren('editActionButton') + editActionButtons: QueryList; @Input() disabled: boolean; @@ -78,6 +84,8 @@ export class ScadaSymbolMetadataTagComponent implements ControlValueAccessor, On @Input() clickActionFunctionCompleter: TbEditorCompleter; + actionTriggers = scadaSymbolActionTriggers; + tagFormGroup: UntypedFormGroup; modelValue: ScadaSymbolTag; @@ -91,11 +99,14 @@ export class ScadaSymbolMetadataTagComponent implements ControlValueAccessor, On } ngOnInit() { - this.tagFormGroup = this.fb.group({ + const controls: {[key: string]: any} = { tag: [null, []], - stateRenderFunction: [null, []], - clickAction: [null, []] - }); + stateRenderFunction: [null, []] + }; + for (const trigger of this.actionTriggers) { + controls[this.actionControlName(trigger)] = [null, []]; + } + this.tagFormGroup = this.fb.group(controls); } registerOnChange(fn: any): void { @@ -116,14 +127,14 @@ export class ScadaSymbolMetadataTagComponent implements ControlValueAccessor, On writeValue(value: ScadaSymbolTag): void { this.modelValue = value; - const clickAction = value?.actions?.click?.actionFunction; - this.tagFormGroup.patchValue( - { - tag: value?.tag, - stateRenderFunction: value?.stateRenderFunction, - clickAction - }, {emitEvent: false} - ); + const patch: {[key: string]: any} = { + tag: value?.tag, + stateRenderFunction: value?.stateRenderFunction + }; + for (const trigger of this.actionTriggers) { + patch[this.actionControlName(trigger)] = value?.actions?.[trigger]?.actionFunction; + } + this.tagFormGroup.patchValue(patch, {emitEvent: false}); } public validate(_c: UntypedFormControl) { @@ -135,15 +146,23 @@ export class ScadaSymbolMetadataTagComponent implements ControlValueAccessor, On }; } + actionControlName(trigger: ScadaSymbolActionTrigger): string { + return trigger + 'Action'; + } + editTagStateRenderFunction(): void { this.openTagFunction('renderFunction', this.editStateRenderFunctionButton); } - editClickAction(): void { - this.openTagFunction('clickAction', this.editClickActionButton); + editAction(trigger: ScadaSymbolActionTrigger): void { + const button = this.editActionButtons + .find(b => b._elementRef.nativeElement.getAttribute('data-trigger') === trigger); + if (button) { + this.openTagFunction(trigger, button); + } } - private openTagFunction(tagFunctionType: 'renderFunction' | 'clickAction', + private openTagFunction(tagFunctionType: 'renderFunction' | ScadaSymbolActionTrigger, button: MatButton) { const trigger = button._elementRef.nativeElement; trigger.scrollIntoView(); @@ -155,8 +174,8 @@ export class ScadaSymbolMetadataTagComponent implements ControlValueAccessor, On if (tagFunctionType === 'renderFunction') { tagFunctionControl = this.tagFormGroup.get('stateRenderFunction'); completer = this.elementStateRenderFunctionCompleter; - } else if (tagFunctionType === 'clickAction') { - tagFunctionControl = this.tagFormGroup.get('clickAction'); + } else { + tagFunctionControl = this.tagFormGroup.get(this.actionControlName(tagFunctionType)); completer = this.clickActionFunctionCompleter; } const scadaSymbolTagFunctionPanelPopover = this.popoverService.displayPopover({ @@ -189,15 +208,17 @@ export class ScadaSymbolMetadataTagComponent implements ControlValueAccessor, On tag: value.tag, stateRenderFunction: value.stateRenderFunction }; - if (value.clickAction) { - this.modelValue.actions = { - click: { - actionFunction: value.clickAction - } - }; - } else { - this.modelValue.actions = null; + let actions: ScadaSymbolTag['actions'] = null; + for (const trigger of this.actionTriggers) { + const actionFunction = value[this.actionControlName(trigger)]; + if (actionFunction) { + actions = actions || {}; + actions[trigger] = { + actionFunction + }; + } } + this.modelValue.actions = actions; this.propagateChange(this.modelValue); } } diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.html index 38d1532b0f..69e5911416 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.html +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.html @@ -20,7 +20,9 @@
scada.tag.tag
scada.state-render-function
-
scada.tag.on-click-action
+ @for (trigger of actionTriggers; track trigger) { +
{{ actionTriggerTranslations.get(trigger) | translate }}
+ }
@if (tagsFormArray().controls.length) {
diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.scss b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.scss index 3463b7ae2f..285dee5fe0 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.scss +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.scss @@ -18,15 +18,10 @@ margin: 12px; .tb-form-table-header-cell { &.tb-tag-header { - flex: 1 1 70%; + flex: 1 1 40%; } - &.tb-state-render-function-header { + &.tb-state-render-function-header, &.tb-action-function-header { flex: 1 1 15%; - } - &.tb-on-click-action-header { - flex: 1 1 15%; - } - &.tb-state-render-function-header, &.tb-on-click-action-header { display: flex; justify-content: flex-end; } diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.ts index f2556d3cf0..c1550d6d39 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.ts @@ -38,7 +38,12 @@ import { UntypedFormGroup, Validator } from '@angular/forms'; -import { ScadaSymbolTag } from '@home/components/widget/lib/scada/scada-symbol.models'; +import { + ScadaSymbolActionTrigger, + scadaSymbolActionTriggers, + scadaSymbolActionTriggerTranslations, + ScadaSymbolTag +} from '@home/components/widget/lib/scada/scada-symbol.models'; import { ScadaSymbolMetadataTagComponent } from '@home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component'; @@ -46,7 +51,7 @@ import { TbEditorCompleter } from '@shared/models/ace/completion.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; const tagIsEmpty = (tag: ScadaSymbolTag): boolean => - !tag.stateRenderFunction && !tag.actions?.click?.actionFunction; + !tag.stateRenderFunction && !scadaSymbolActionTriggers.some(trigger => tag.actions?.[trigger]?.actionFunction); @Component({ selector: 'tb-scada-symbol-metadata-tags', @@ -87,6 +92,10 @@ export class ScadaSymbolMetadataTagsComponent implements ControlValueAccessor, O @Input() clickActionFunctionCompleter: TbEditorCompleter; + actionTriggers = scadaSymbolActionTriggers; + + actionTriggerTranslations = scadaSymbolActionTriggerTranslations; + tagsFormGroup: UntypedFormGroup; private modelValue: ScadaSymbolTag[]; @@ -188,12 +197,12 @@ export class ScadaSymbolMetadataTagsComponent implements ControlValueAccessor, O }); } - editTagClickAction(tag: string): void { + editTagAction(tag: string, trigger: ScadaSymbolActionTrigger): void { setTimeout(() => { const tags: ScadaSymbolTag[] = this.tagsFormGroup.get('tags').value; const index = tags.findIndex(t => t.tag === tag); const tagComponent = this.metadataTags.get(index); - tagComponent?.editClickAction(); + tagComponent?.editAction(trigger); }); } @@ -208,12 +217,14 @@ export class ScadaSymbolMetadataTagsComponent implements ControlValueAccessor, O const found = result.find(t => t.tag === tag.tag); if (found) { found.stateRenderFunction = tag.stateRenderFunction; - if (tag.actions?.click?.actionFunction) { - found.actions = { - click: { - actionFunction: tag.actions.click.actionFunction - } - }; + for (const trigger of scadaSymbolActionTriggers) { + const actionFunction = tag.actions?.[trigger]?.actionFunction; + if (actionFunction) { + found.actions = found.actions || {}; + found.actions[trigger] = { + actionFunction + }; + } } } } diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts index a893100791..de5da80038 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts @@ -36,7 +36,11 @@ import { Validators } from '@angular/forms'; import { PageComponent } from '@shared/components/page.component'; -import { emptyMetadata, ScadaSymbolMetadata } from '@home/components/widget/lib/scada/scada-symbol.models'; +import { + emptyMetadata, + ScadaSymbolActionTrigger, + ScadaSymbolMetadata +} from '@home/components/widget/lib/scada/scada-symbol.models'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { ToggleHeaderOption } from '@shared/components/toggle-header.component'; @@ -205,9 +209,9 @@ export class ScadaSymbolMetadataComponent extends PageComponent implements OnIni this.symbolMetadataTags.editTagStateRenderFunction(tag); } - editTagClickAction(tag: string): void { + editTagAction(tag: string, trigger: ScadaSymbolActionTrigger): void { this.selectedOption = 'tags'; - this.symbolMetadataTags.editTagClickAction(tag); + this.symbolMetadataTags.editTagAction(tag, trigger); } public validate(c: UntypedFormControl) { diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts index fe59eb8dc3..14378a9a98 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts @@ -24,6 +24,8 @@ import { setupTagPanelTooltip } from '@home/pages/scada-symbol/scada-symbol-tooltip.components'; import { + ScadaSymbolActionTrigger, + scadaSymbolActionTriggers, ScadaSymbolBehavior, ScadaSymbolBehaviorType, scadaSymbolContentData, @@ -51,9 +53,9 @@ export interface ScadaSymbolData { export interface ScadaSymbolEditObjectCallbacks { tagHasStateRenderFunction: (tag: string) => boolean; - tagHasClickAction: (tag: string) => boolean; + tagHasAction: (tag: string, trigger: ScadaSymbolActionTrigger) => boolean; editTagStateRenderFunction: (tag: string) => void; - editTagClickAction: (tag: string) => void; + editTagAction: (tag: string, trigger: ScadaSymbolActionTrigger) => void; tagsUpdated: (tags: string[]) => void; hasHiddenElements?: (hasHidden: boolean) => void; onSymbolEditObjectDirty: (dirty: boolean) => void; @@ -423,16 +425,16 @@ export class ScadaSymbolEditObject { return this.callbacks.tagHasStateRenderFunction(tag); } - public tagHasClickAction(tag: string): boolean { - return this.callbacks.tagHasClickAction(tag); + public tagHasAction(tag: string, trigger: ScadaSymbolActionTrigger): boolean { + return this.callbacks.tagHasAction(tag, trigger); } public editTagStateRenderFunction(tag: string) { this.callbacks.editTagStateRenderFunction(tag); } - public editTagClickAction(tag: string) { - this.callbacks.editTagClickAction(tag); + public editTagAction(tag: string, trigger: ScadaSymbolActionTrigger) { + this.callbacks.editTagAction(tag, trigger); } public setDirty(dirty: boolean) { @@ -645,23 +647,27 @@ export class ScadaSymbolElement { } } - public hasClickAction(): boolean { + public hasAction(trigger: ScadaSymbolActionTrigger): boolean { if (this.hasTag()) { - return this.editObject.tagHasClickAction(this.tag); + return this.editObject.tagHasAction(this.tag, trigger); } else { return false; } } + public hasAnyAction(): boolean { + return scadaSymbolActionTriggers.some(trigger => this.hasAction(trigger)); + } + public editStateRenderFunction() { if (this.hasTag()) { this.editObject.editTagStateRenderFunction(this.tag); } } - public editClickAction() { + public editAction(trigger: ScadaSymbolActionTrigger) { if (this.hasTag()) { - this.editObject.editTagClickAction(this.tag); + this.editObject.editTagAction(this.tag, trigger); } } diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.components.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.components.ts index f4065f8f93..1e04d69ac9 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.components.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.components.ts @@ -31,6 +31,11 @@ import { ViewEncapsulation } from '@angular/core'; import { ScadaSymbolElement } from '@home/pages/scada-symbol/scada-symbol-editor.models'; +import { + ScadaSymbolActionTrigger, + scadaSymbolActionTriggers, + scadaSymbolActionTriggerTranslations +} from '@home/components/widget/lib/scada/scada-symbol.models'; import { CommonModule } from '@angular/common'; import { SharedModule } from '@shared/shared.module'; import { ENTER } from '@angular/cdk/keycodes'; @@ -241,7 +246,7 @@ class ScadaSymbolTagPanelComponent extends ScadaSymbolPanelComponent implements ngOnInit() { if (this.symbolElement?.readonly) { - this.displayTagSettings = (this.symbolElement?.hasStateRenderFunction() || this.symbolElement?.hasClickAction()); + this.displayTagSettings = (this.symbolElement?.hasStateRenderFunction() || this.symbolElement?.hasAnyAction()); } } @@ -387,26 +392,28 @@ class ScadaSymbolRemoveTagConfirmComponent extends ScadaSymbolPanelComponent imp action.add } - @if (!symbolElement?.readonly || hasClickAction) { -
scada.tag.on-click-action
- } - @if (hasClickAction) { - - } - @if (!hasClickAction && !symbolElement?.readonly) { - + @for (trigger of actionTriggers; track trigger) { + @if (!symbolElement?.readonly || hasActions[trigger]) { +
{{ actionTriggerTranslations.get(trigger) | translate }}
+ } + @if (hasActions[trigger]) { + + } + @if (!hasActions[trigger] && !symbolElement?.readonly) { + + } }
`, styleUrls: ['./scada-symbol-tooltip.component.scss'], @@ -415,9 +422,13 @@ class ScadaSymbolRemoveTagConfirmComponent extends ScadaSymbolPanelComponent imp }) class ScadaSymbolTagSettingsComponent extends ScadaSymbolPanelComponent implements OnInit, AfterViewInit { + actionTriggers = scadaSymbolActionTriggers; + + actionTriggerTranslations = scadaSymbolActionTriggerTranslations; + hasStateRenderFunction = false; - hasClickAction = false; + hasActions: {[trigger in ScadaSymbolActionTrigger]?: boolean} = {}; constructor(public element: ElementRef) { super(element); @@ -429,15 +440,17 @@ class ScadaSymbolTagSettingsComponent extends ScadaSymbolPanelComponent implemen updateFunctionsState() { this.hasStateRenderFunction = this.symbolElement.hasStateRenderFunction(); - this.hasClickAction = this.symbolElement.hasClickAction(); + for (const trigger of this.actionTriggers) { + this.hasActions[trigger] = this.symbolElement.hasAction(trigger); + } } editStateRenderFunction() { this.symbolElement.editStateRenderFunction(); } - editClickAction() { - this.symbolElement.editClickAction(); + editAction(trigger: ScadaSymbolActionTrigger) { + this.symbolElement.editAction(trigger); } } diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts index f2a358fde7..2a51f0f124 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts @@ -33,6 +33,7 @@ import { Subject } from 'rxjs'; import { ScadaSymbolData, ScadaSymbolEditObjectCallbacks } from '@home/pages/scada-symbol/scada-symbol-editor.models'; import { parseScadaSymbolMetadataFromContent, + ScadaSymbolActionTrigger, ScadaSymbolMetadata, ScadaSymbolObjectSettings, updateScadaSymbolMetadataInContent @@ -330,11 +331,11 @@ export class ScadaSymbolComponent extends PageComponent return false; } - tagHasClickAction(tag: string): boolean { + tagHasAction(tag: string, trigger: ScadaSymbolActionTrigger): boolean { const metadata: ScadaSymbolMetadata = this.scadaSymbolFormGroup.get('metadata').value; if (metadata.tags) { const found = metadata.tags.find(t => t.tag === tag); - return !!found?.actions?.click?.actionFunction; + return !!found?.actions?.[trigger]?.actionFunction; } return false; } @@ -343,8 +344,8 @@ export class ScadaSymbolComponent extends PageComponent this.symbolMetadata.editTagStateRenderFunction(tag); } - editTagClickAction(tag: string) { - this.symbolMetadata.editTagClickAction(tag); + editTagAction(tag: string, trigger: ScadaSymbolActionTrigger) { + this.symbolMetadata.editTagAction(tag, trigger); } onSymbolEditObjectDirty(dirty: boolean) { diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 1007fa3eea..83cf304d72 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3888,6 +3888,8 @@ "tag": { "tag": "Tag", "on-click-action": "On click action", + "on-double-click-action": "On double click action", + "on-right-click-action": "On right click action", "no-tags": "No tags configured", "delete-tag-text": "Are you sure you want to delete tag
{{tag}} from {{elementType}} element?", "update-tag": "Update tag", From d872e6b61c7cd2161f43507fdc0eacf496ef4c21 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Fri, 10 Jul 2026 14:23:13 +0200 Subject: [PATCH 2/2] Action execution fixes --- .../widget/lib/scada/scada-symbol.models.ts | 37 +++++++++-- ...l-metadata-tag-function-panel.component.ts | 7 +- .../scada-symbol-metadata-tag.component.ts | 4 +- .../scada-symbol-metadata-tags.component.html | 2 +- .../scada-symbol-metadata-tags.component.ts | 2 +- .../scada-symbol-metadata.component.html | 2 +- .../scada-symbol-metadata.component.ts | 12 ++-- .../scada-symbol-editor.models.ts | 4 +- .../en_US/scada/tag_contextmenu_action_fn.md | 65 +++++++++++++++++++ .../en_US/scada/tag_dblclick_action_fn.md | 65 +++++++++++++++++++ 10 files changed, 178 insertions(+), 22 deletions(-) create mode 100644 ui-ngx/src/assets/help/en_US/scada/tag_contextmenu_action_fn.md create mode 100644 ui-ngx/src/assets/help/en_US/scada/tag_dblclick_action_fn.md diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts index 4e6132926c..3b454a1961 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts @@ -120,6 +120,10 @@ export type ScadaSymbolActionTrigger = 'click' | 'dblclick' | 'contextmenu'; export const scadaSymbolActionTriggers: ScadaSymbolActionTrigger[] = ['click', 'dblclick', 'contextmenu']; +// Max interval between clicks of a double click: a click action is deferred by this amount +// when the same tag also has a dblclick action, so it can be canceled by the second click. +const dblClickDelay = 300; + export const scadaSymbolActionTriggerTranslations = new Map( [ ['click', 'scada.tag.on-click-action'], @@ -128,6 +132,14 @@ export const scadaSymbolActionTriggerTranslations = new Map( + [ + ['click', 'scada/tag_click_action_fn'], + ['dblclick', 'scada/tag_dblclick_action_fn'], + ['contextmenu', 'scada/tag_contextmenu_action_fn'] + ] +); + export type ScadaSymbolActionFunction = (ctx: ScadaSymbolContext, element: Element, event: Event) => void; export interface ScadaSymbolAction { actionFunction?: string; @@ -679,16 +691,29 @@ export class ScadaSymbolObject { for (const tag of this.metadata.tags) { if (tag.actions) { const elements = this.svgShape.find(`[tb\\:tag="${tag.tag}"]`); + const deferClick = !!tag.actions.click && !!tag.actions.dblclick; for (const trigger of Object.keys(tag.actions)) { const action = tag.actions[trigger]; elements.forEach(element => { element.attr('cursor', 'pointer'); - element.on(trigger, (event) => { - if (trigger === 'contextmenu') { - event.preventDefault(); - } - action.action(this.context, element, event); - }); + if (trigger === 'click' && deferClick) { + let clickTimeout: ReturnType = null; + element.on('click', (event: MouseEvent) => { + clearTimeout(clickTimeout); + if (event.detail === 1) { + clickTimeout = setTimeout(() => { + action.action(this.context, element, event); + }, dblClickDelay); + } + }); + } else { + element.on(trigger, (event) => { + if (trigger === 'contextmenu') { + event.preventDefault(); + } + action.action(this.context, element, event); + }); + } }); } } diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component.ts index 10da5339b0..9a4869b901 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component.ts @@ -30,12 +30,13 @@ import { WidgetService } from '@core/http/widget.service'; import { TbEditorCompleter } from '@shared/models/ace/completion.models'; import { AceHighlightRules } from '@shared/models/ace/ace.models'; import { - scadaSymbolClickActionHighlightRules, + scadaSymbolActionFunctionHighlightRules, scadaSymbolRenderFunctionHighlightRules } from '@home/pages/scada-symbol/scada-symbol-editor.models'; import { JsFuncComponent } from '@shared/components/js-func.component'; import { ScadaSymbolActionTrigger, + scadaSymbolActionTriggerHelpIds, scadaSymbolActionTriggerTranslations } from '@home/components/widget/lib/scada/scada-symbol.models'; @@ -105,8 +106,8 @@ export class ScadaSymbolMetadataTagFunctionPanelComponent implements OnInit, Aft } else { this.panelTitle = scadaSymbolActionTriggerTranslations.get(this.tagFunctionType); this.tagFunctionArgs = ['ctx', 'element', 'event']; - this.highlightRules = scadaSymbolClickActionHighlightRules; - this.tagFunctionHelpId = 'scada/tag_click_action_fn'; + this.highlightRules = scadaSymbolActionFunctionHighlightRules; + this.tagFunctionHelpId = scadaSymbolActionTriggerHelpIds.get(this.tagFunctionType); } } diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.ts index 35b9c395b1..4ab6f2254d 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.ts @@ -82,7 +82,7 @@ export class ScadaSymbolMetadataTagComponent implements ControlValueAccessor, On elementStateRenderFunctionCompleter: TbEditorCompleter; @Input() - clickActionFunctionCompleter: TbEditorCompleter; + actionFunctionCompleter: TbEditorCompleter; actionTriggers = scadaSymbolActionTriggers; @@ -176,7 +176,7 @@ export class ScadaSymbolMetadataTagComponent implements ControlValueAccessor, On completer = this.elementStateRenderFunctionCompleter; } else { tagFunctionControl = this.tagFormGroup.get(this.actionControlName(tagFunctionType)); - completer = this.clickActionFunctionCompleter; + completer = this.actionFunctionCompleter; } const scadaSymbolTagFunctionPanelPopover = this.popoverService.displayPopover({ trigger, diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.html index 69e5911416..84701c934f 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.html +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.html @@ -30,7 +30,7 @@
@if (!$last) { diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.ts index c1550d6d39..0180e03d25 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.ts @@ -90,7 +90,7 @@ export class ScadaSymbolMetadataTagsComponent implements ControlValueAccessor, O elementStateRenderFunctionCompleter: TbEditorCompleter; @Input() - clickActionFunctionCompleter: TbEditorCompleter; + actionFunctionCompleter: TbEditorCompleter; actionTriggers = scadaSymbolActionTriggers; diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html index 30dbe4f734..47f8eabc3b 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html @@ -95,7 +95,7 @@ diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts index de5da80038..e2d0798e82 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts @@ -50,7 +50,7 @@ import { } from '@home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component'; import { TbEditorCompleter } from '@shared/models/ace/completion.models'; import { - clickActionFunctionCompletions, + actionFunctionCompletions, elementStateRenderFunctionCompletions, generalStateRenderFunctionCompletions, scadaSymbolContextCompletion, @@ -126,7 +126,7 @@ export class ScadaSymbolMetadataComponent extends PageComponent implements OnIni generalStateRenderFunctionCompleter: TbEditorCompleter; elementStateRenderFunctionCompleter: TbEditorCompleter; - clickActionFunctionCompleter: TbEditorCompleter; + actionFunctionCompleter: TbEditorCompleter; highlightRules = scadaSymbolGeneralStateHighlightRules; @@ -244,11 +244,11 @@ export class ScadaSymbolMetadataComponent extends PageComponent implements OnIni } else { this.elementStateRenderFunctionCompleter.updateCompletions(elementStateRender); } - const clickAction = clickActionFunctionCompletions(contextCompleter); - if (!this.clickActionFunctionCompleter) { - this.clickActionFunctionCompleter = new TbEditorCompleter(clickAction); + const actionFunction = actionFunctionCompletions(contextCompleter); + if (!this.actionFunctionCompleter) { + this.actionFunctionCompleter = new TbEditorCompleter(actionFunction); } else { - this.clickActionFunctionCompleter.updateCompletions(clickAction); + this.actionFunctionCompleter.updateCompletions(actionFunction); } } } diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts index 14378a9a98..cc3c77baf3 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts @@ -1087,7 +1087,7 @@ export const scadaSymbolRenderFunctionHighlightRules: AceHighlightRules = { ...scadaSymbolElementPropertyHighlightRules }; -export const scadaSymbolClickActionHighlightRules: AceHighlightRules = { +export const scadaSymbolActionFunctionHighlightRules: AceHighlightRules = { start: [ scadaSymbolCtxObjectHighlightRule, scadaSymbolElementHighlightRule, @@ -1120,7 +1120,7 @@ export const elementStateRenderFunctionCompletions = (ctxCompletion: TbEditorCom } }); -export const clickActionFunctionCompletions = (ctxCompletion: TbEditorCompletion): TbEditorCompletions => { +export const actionFunctionCompletions = (ctxCompletion: TbEditorCompletion): TbEditorCompletions => { const completions = elementStateRenderFunctionCompletions(ctxCompletion); completions.event = { meta: 'argument', diff --git a/ui-ngx/src/assets/help/en_US/scada/tag_contextmenu_action_fn.md b/ui-ngx/src/assets/help/en_US/scada/tag_contextmenu_action_fn.md new file mode 100644 index 0000000000..ca68bb3bf0 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/scada/tag_contextmenu_action_fn.md @@ -0,0 +1,65 @@ +#### Symbol tag right click action function + +
+
+ +*function (ctx, element, event): void* + +A JavaScript function invoked when user right-clicks on SVG element with specific tag. + +**Parameters:** + +
    +
  • ctx: ScadaSymbolContext - Context of the SCADA symbol. +
  • +
  • element: Element - SVG.js element.
    + See the examples below to learn how to manipulate and animate elements.
    +
  • +
  • event: Event - DOM event. +
  • +
+ +
+ +The browser context menu is automatically suppressed on the element, so the configured action is executed instead. +Note that on touch devices the right click action is typically triggered by a long press. + +##### Example + +**Invoke widget right click action** + +
+ +This JavaScript snippet demonstrates triggering a widget action using the ScadaSymbolContext API when the right click event occurs. The widget action will be linked to the behaviorId 'rightClick', which defines the action that will be executed upon the event. +The behavior of this action depends on the type of widget action configured in the ThingsBoard platform (e.g., navigating to a dashboard state, updating the current state, opening a URL, etc.). + +
+ +```javascript +ctx.api.callAction(event, 'rightClick'); // Trigger widget action 'rightClick' on event +{:copy-code} +``` + +
+ +*Example Use Case* + +- **Open details popup:** If the 'rightClick' action is configured to open a popup with device details, right-clicking the element shows contextual information, while the left click keeps its primary action. +- **Custom context menu:** A custom-action widget action can render your own context menu for the element in place of the browser one. + +
+ +You can also execute an action behavior directly and pass a value, for example: + +```javascript +var running = ctx.values.running; // Current running status from context + +// Request device shutdown with observer callbacks for error handling +ctx.api.callAction(event, 'shutdown', undefined, { + error: () => { + // Action failed; keep the current state + ctx.api.setValue('running', running); + } +}); +{:copy-code} +``` diff --git a/ui-ngx/src/assets/help/en_US/scada/tag_dblclick_action_fn.md b/ui-ngx/src/assets/help/en_US/scada/tag_dblclick_action_fn.md new file mode 100644 index 0000000000..a5a41ebe9c --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/scada/tag_dblclick_action_fn.md @@ -0,0 +1,65 @@ +#### Symbol tag double click action function + +
+
+ +*function (ctx, element, event): void* + +A JavaScript function invoked when user double-clicks on SVG element with specific tag. + +**Parameters:** + +
    +
  • ctx: ScadaSymbolContext - Context of the SCADA symbol. +
  • +
  • element: Element - SVG.js element.
    + See the examples below to learn how to manipulate and animate elements.
    +
  • +
  • event: Event - DOM event. +
  • +
+ +
+ +If the same tag also has an *on click* action configured, the click action is automatically deferred for a short interval +and cancelled when a double click is detected — only the double click action is executed. + +##### Example + +**Invoke widget double click action** + +
+ +This JavaScript snippet demonstrates triggering a widget action using the ScadaSymbolContext API when the double click event occurs. The widget action will be linked to the behaviorId 'doubleClick', which defines the action that will be executed upon the event. +The behavior of this action depends on the type of widget action configured in the ThingsBoard platform (e.g., navigating to a dashboard state, updating the current state, opening a URL, etc.). + +
+ +```javascript +ctx.api.callAction(event, 'doubleClick'); // Trigger widget action 'doubleClick' on event +{:copy-code} +``` + +
+ +*Example Use Case* + +- **Navigate to Dashboard State:** If the 'doubleClick' action is configured to navigate to a detailed dashboard state, the user will be redirected to that state when double-clicking on the element, while a single click may be reserved for a quick toggle action. +- **Open URL:** If configured to open a URL, double-clicking on the element will take the user to a specified web resource. + +
+ +You can also execute an action behavior directly and pass a value, for example: + +```javascript +var running = ctx.values.running; // Current running status from context + +// Toggle the device state with observer callbacks for next and error handling +ctx.api.callAction(event, 'switchState', !running, { + error: () => { + // Action failed; keep the current state + ctx.api.setValue('running', running); + } +}); +{:copy-code} +```