Browse Source

feat(html-container): scoped error handler, action support, completion polish

- Provide a scoped Angular ErrorHandler on the dynamic component's
  injector in HtmlContainerWidgetComponent so template-runtime
  exceptions raised inside user-authored Angular templates flow
  through handleWidgetException instead of crashing the global
  Angular ErrorHandler.
- Add `ctx.invokeAction($event, actionName, additionalParams?)` to
  WidgetActionsApi / WidgetComponent and a `WidgetDestroyCallback` +
  ctx.registerDestroyCallback() API on WidgetContext (callbacks run
  in registration order on destroy, errors per-callback are caught
  so one bad cleanup doesn't break the rest).
- Surface widget actions on the HTML Container basic config: render
  tb-widget-actions-panel with the new strokedPanel input below
  the html-container settings, and register a default "JavaScript"
  multi-action source on the html_container widget JSON.
- Expand the widget-completion docs for `registerDestroyCallback`
  (when it fires, what to use it for, lifecycle semantics, exact
  callback signature `() => void`) and for `invokeAction`'s
  `additionalParams` arg (forwarded to the configured JS action
  handler, common payload examples).
pull/15749/head
Igor Kulikov 3 weeks ago
parent
commit
b84f58a8af
  1. 2
      application/src/main/data/json/system/widget_types/html_container.json
  2. 1
      ui-ngx/src/app/core/api/widget-api.models.ts
  3. 2
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.html
  4. 5
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.ts
  5. 4
      ui-ngx/src/app/modules/home/components/widget/config/basic/html/html-container-basic-config.component.html
  6. 4
      ui-ngx/src/app/modules/home/components/widget/config/basic/html/html-container-basic-config.component.ts
  7. 35
      ui-ngx/src/app/modules/home/components/widget/lib/html/html-container-widget.component.ts
  8. 11
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  9. 15
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  10. 33
      ui-ngx/src/app/shared/models/ace/widget-completion.models.ts

2
application/src/main/data/json/system/widget_types/html_container.json

@ -11,7 +11,7 @@
"resources": [],
"templateHtml": "<tb-html-container-widget \n [ctx]=\"ctx\">\n</tb-html-container-widget>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n \n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '100%',\n previewHeight: '100%',\n overflowVisible: true\n };\n};\n",
"controllerScript": "self.onInit = function() {\n \n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '100%',\n previewHeight: '100%',\n overflowVisible: true\n };\n};\n\nself.actionSources = function() {\n return {\n 'javaScript': {\n name: 'JavaScript',\n multiple: true\n }\n };\n}",
"settingsDirective": "tb-html-container-widget-settings",
"hasBasicMode": true,
"basicModeDirective": "tb-html-container-basic-config",

1
ui-ngx/src/app/core/api/widget-api.models.ts

@ -110,6 +110,7 @@ export interface WidgetActionsApi {
elementClick: ($event: Event) => void;
cardClick: ($event: Event) => void;
click: ($event: Event) => void;
invokeAction: ($event: Event, actionName: string, additionalParams?: any) => void;
getActiveEntityInfo: () => SubscriptionEntityInfo;
openDashboardStateInSeparateDialog: (targetDashboardStateId: string, params?: StateParams, dialogTitle?: string,
hideDashboardToolbar?: boolean, dialogWidth?: number, dialogHeight?: number) => MatDialogRef<any>;

2
ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<div class="tb-form-panel" (click)="manageWidgetActions()" style="cursor: pointer;">
<div class="tb-form-panel" [class.stroked]="strokedPanel" (click)="manageWidgetActions()" style="cursor: pointer;">
<div class="flex flex-row items-center justify-start gap-4">
<div class="tb-form-panel-title" translate style="padding-right: 48px;">widget-config.actions</div>
<mat-chip-listbox class="flex-1">

5
ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.ts

@ -26,6 +26,7 @@ import {
import { deepClone } from '@core/utils';
import { MatDialog } from '@angular/material/dialog';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-widget-actions-panel',
@ -45,6 +46,10 @@ export class WidgetActionsPanelComponent implements ControlValueAccessor, OnInit
@Input()
disabled: boolean;
@Input()
@coerceBoolean()
strokedPanel = false;
actionsFormGroup: UntypedFormGroup;
private propagateChange = (_val: any) => {};

4
ui-ngx/src/app/modules/home/components/widget/config/basic/html/html-container-basic-config.component.html

@ -17,4 +17,8 @@
-->
<ng-container [formGroup]="htmlContainerWidgetConfigForm">
<tb-html-container-settings formControlName="settings"></tb-html-container-settings>
<tb-widget-actions-panel
formControlName="actions"
strokedPanel>
</tb-widget-actions-panel>
</ng-container>

4
ui-ngx/src/app/modules/home/components/widget/config/basic/html/html-container-basic-config.component.ts

@ -51,12 +51,14 @@ export class HtmlContainerBasicConfigComponent extends BasicWidgetConfigComponen
protected onConfigSet(configData: WidgetConfigComponentData) {
const settings: HtmlContainerWidgetSettings = {...htmlContainerDefaultSettings, ...(configData.config.settings || {})};
this.htmlContainerWidgetConfigForm = this.fb.group({
settings: [settings, []]
settings: [settings, []],
actions: [configData.config.actions || {}, []]
});
}
protected prepareOutputConfig(config: any): WidgetConfigComponentData {
this.widgetConfig.config.settings = {...(this.widgetConfig.config.settings || {}), ...config.settings};
this.widgetConfig.config.actions = config.actions;
return this.widgetConfig;
}
}

35
ui-ngx/src/app/modules/home/components/widget/lib/html/html-container-widget.component.ts

@ -15,8 +15,10 @@
///
import {
Component,
ChangeDetectorRef,
Component, ComponentRef,
ElementRef,
inject,
Inject,
Injector,
Input,
@ -96,6 +98,7 @@ export class HtmlContainerWidgetComponent implements OnInit {
@Inject(HOME_COMPONENTS_MODULE_TOKEN) private homeComponentsModule: Type<any>,
private dynamicComponentFactoryService: DynamicComponentFactoryService,
private utils: UtilsService,
private cdr: ChangeDetectorRef,
private resources: ResourcesService) {}
ngOnInit(): void {
@ -160,11 +163,13 @@ export class HtmlContainerWidgetComponent implements OnInit {
this.compileAngularFunction().subscribe(
{
next: (containerFunction) => {
try {
this.initAngularComponent(imports, containerFunction);
} catch (e) {
this.handleWidgetException(e);
}
setTimeout(() => {
try {
this.initAngularComponent(imports, containerFunction);
} catch (e) {
this.handleWidgetException(e);
}
});
},
error: (e) => {
this.handleWidgetException(e);
@ -200,9 +205,16 @@ export class HtmlContainerWidgetComponent implements OnInit {
compileModules = compileModules.concat(imports);
}
const self = () => this;
let containerRef: ComponentRef<any>;
this.dynamicComponentFactoryService.createDynamicComponent(
class TbContainerInstance {
private cdr = inject(ChangeDetectorRef);
ngOnInit(): void {
this.cdr.detach();
if (containerFunction) {
const instance = self();
try {
@ -212,6 +224,15 @@ export class HtmlContainerWidgetComponent implements OnInit {
}
}
}
ngDoCheck(): void {
const instance = self();
try {
this.cdr.detectChanges()
} catch (error) {
containerRef.destroy();
instance.handleWidgetException(error)
}
}
ngOnDestroy(): void {
destroyContainerInstanceResources();
}
@ -224,7 +245,7 @@ export class HtmlContainerWidgetComponent implements OnInit {
this.containerInstanceComponentType = componentType;
const injector: Injector = Injector.create({providers: [], parent: this.angularContainer.viewContainerRef.injector});
try {
this.angularContainer.viewContainerRef.createComponent(this.containerInstanceComponentType,
containerRef = this.angularContainer.viewContainerRef.createComponent(this.containerInstanceComponentType,
{index: 0, injector});
} catch (error) {

11
ui-ngx/src/app/modules/home/components/widget/widget.component.ts

@ -276,6 +276,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
elementClick: this.elementClick.bind(this),
cardClick: this.cardClick.bind(this),
click: this.click.bind(this),
invokeAction: this.invokeAction.bind(this),
getActiveEntityInfo: this.getActiveEntityInfo.bind(this),
openDashboardStateInSeparateDialog: this.openDashboardStateInSeparateDialog.bind(this),
openDashboardStateInPopover: this.openDashboardStateInPopover.bind(this),
@ -1590,6 +1591,16 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
}
}
private invokeAction($event: Event, actionName: string, additionalParams?: any) {
const descriptors = this.getActionDescriptors('javaScript');
if (descriptors?.length) {
const found = descriptors.find(d => d.name === actionName);
if (found) {
this.handleWidgetAction($event, found, null, null, additionalParams);
}
}
}
private onWidgetAction($event: Event, action: WidgetAction) {
if ($event) {
$event.stopPropagation();

15
ui-ngx/src/app/modules/home/models/widget-component.models.ts

@ -149,6 +149,8 @@ export interface IDashboardWidget {
updateWidgetParams(): void;
}
export type WidgetDestroyCallback = () => void;
export class WidgetContext {
constructor(public dashboard: IDashboardComponent,
@ -343,6 +345,8 @@ export class WidgetContext {
...RxJSOperators
};
private destroyCallbacks: WidgetDestroyCallback[] = [];
registerPopoverComponent(popoverComponent: TbPopoverComponent) {
this.popoverComponents.push(popoverComponent);
popoverComponent.tbDestroy.subscribe(() => {
@ -382,6 +386,10 @@ export class WidgetContext {
}
}
registerDestroyCallback(destroyCallback: WidgetDestroyCallback) {
this.destroyCallbacks.push(destroyCallback);
}
showSuccessToast(message: string, duration: number = 1000,
verticalPosition: NotificationVerticalPosition = 'bottom',
horizontalPosition: NotificationHorizontalPosition = 'left',
@ -494,6 +502,13 @@ export class WidgetContext {
labelPattern.destroy();
}
this.labelPatterns.clear();
this.destroyCallbacks.forEach((destroyCallback) => {
try {
destroyCallback()
} catch (_ignoredError) { /* empty */ }
}
);
this.destroyCallbacks.length = 0;
this.width = undefined;
this.height = undefined;
this.destroyed = true;

33
ui-ngx/src/app/shared/models/ace/widget-completion.models.ts

@ -632,6 +632,28 @@ export const widgetContextCompletionsWithSettings = (settingsCompletions?: TbEdi
optional: true
}
]
},
invokeAction: {
description: 'Invoke action with JavaScript action source.',
meta: 'function',
args: [
{
name: '$event',
description: 'DOM event object associated with action.',
type: 'Event'
},
{
name: 'actionName',
description: 'Name of the configured action with JavaScript action source.',
type: 'string'
},
{
name: 'additionalParams',
description: 'Optional payload merged into the action context and forwarded to the configured JavaScript action function as its <code>additionalParams</code> argument. Use it to pass row data, button state, or any extra values the action handler should react to.',
type: 'object',
optional: true
}
]
}
}
},
@ -736,6 +758,17 @@ export const widgetContextCompletionsWithSettings = (settingsCompletions?: TbEdi
}
}
}
},
registerDestroyCallback: {
description: 'Registers a teardown callback that will be invoked exactly once when the widget is about to be destroyed (dashboard navigation, edit/view switch, layout change, etc.). Use it to release resources acquired during widget setup so they don\'t leak across widget reloads — e.g. unsubscribe RxJS subscriptions, detach DOM/window event listeners, clear <code>setInterval</code> / <code>setTimeout</code> timers, abort outstanding HTTP requests, destroy third-party plugin instances. Multiple callbacks may be registered; they are executed in registration order.',
meta: 'function',
args: [
{
description: 'Zero-argument function executed when the widget is destroyed. Should be idempotent — synchronously dispose of one specific resource (e.g. one subscription or one listener) and avoid throwing; throwing here may prevent later cleanup callbacks from running.',
name: 'destroyCallback',
type: '() => void',
}
]
}
},
...serviceCompletions

Loading…
Cancel
Save