UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

1 lines 94.5 kB
{"version":3,"file":"c8y-ngx-components-widgets-implementations-html-widget.mjs","sources":["../../widgets/implementations/html-widget/html-widget.model.ts","../../widgets/implementations/html-widget/webcomponent-template.ts","../../widgets/implementations/html-widget/legacy-template.ts","../../widgets/implementations/html-widget/html-widget-config.service.ts","../../widgets/implementations/html-widget/advanced-settings/advanced-settings.component.ts","../../widgets/implementations/html-widget/advanced-settings/advanced-settings.component.html","../../widgets/implementations/html-widget/html-ai-chat-tool-details.component.ts","../../widgets/implementations/html-widget/html-ai-chat-tool-details.component.html","../../widgets/implementations/html-widget/html-frame/html-frame.component.ts","../../widgets/implementations/html-widget/html-frame/html-frame.component.html","../../widgets/implementations/html-widget/widget-code-editor-section/widget-code-editor.component.ts","../../widgets/implementations/html-widget/widget-code-editor-section/widget-code-editor.component.html","../../widgets/implementations/html-widget/html-widget-config.component.ts","../../widgets/implementations/html-widget/html-widget-config.component.html","../../widgets/implementations/html-widget/html-widget-properties-selector/html-widget-properties-selector.component.ts","../../widgets/implementations/html-widget/html-widget-properties-selector/html-widget-properties-selector.component.html","../../widgets/implementations/html-widget/html-widget.component.ts","../../widgets/implementations/html-widget/html-widget.component.html","../../widgets/implementations/html-widget/c8y-ngx-components-widgets-implementations-html-widget.ts"],"sourcesContent":["import type { IIdentified, IManagedObject } from '@c8y/client';\nimport type { WidgetSettings } from '@c8y/ngx-components';\nimport type { InterpolationParameters, Translation } from '@ngx-translate/core';\nimport type {\n AssetPropertyMappings,\n ContextWidgetConfig\n} from '@c8y/ngx-components/context-dashboard';\n\nexport interface HtmlWidgetConfig extends ContextWidgetConfig {\n device?: IIdentified | null;\n config: HtmlWidget;\n settings?: WidgetSettings;\n properties?: AssetPropertyMappings;\n\n /**\n * On HTML WIdget 1.0 this property was used to store the HTML code.\n * It is not used anymore, but we need to keep it for backward compatibility.\n * The HTML code is now stored in the config property.\n * @deprecated Use config.code instead.\n */\n html?: any;\n}\n\nexport type C8yProperties = Array<PathProperty | ComputedProperty>;\n\nexport interface HtmlWidget {\n css: string;\n code: string;\n options: HtmlWidgetOptions;\n legacy: boolean;\n devMode: boolean;\n latestCodeHash?: string;\n}\n\nexport interface HtmlWidgetOptions {\n cssEncapsulation: boolean;\n advancedSecurity: boolean;\n}\n\nexport interface WebcomponentContext extends HTMLElement {\n c8yContext: IManagedObject;\n c8yProperties?: Record<string, unknown>;\n c8yTranslate: (\n key: string | string[],\n interpolateParams?: InterpolationParameters\n ) => Translation;\n}\n\nexport interface PathProperty {\n name: string;\n path: string;\n query?: never;\n reducer?: never;\n}\n\nexport interface ComputedProperty {\n name: string;\n path?: never;\n query: string;\n reducer?: string;\n}\n\nexport const INITIAL_HTML_FORMATTED = `<div>\n <h2>Hello from <span class=\"branded\">HTML widget</span></h2>\n\n <p class=\"m-t-16\">\n \\$\\{this.c8yTranslate('You can use HTML and JavaScript template literals:')\\}\n </p>\n\n <p class=\"m-t-16\">\n <b>Context properties:</b> Access the assigned asset's properties via <code>c8yContext</code>: <br>\n <tt>\\$\\{this.c8yContext ? this.c8yTranslate('Selected asset: {{ assetName }}', { assetName: this.c8yContext.name }) : this.c8yTranslate('No asset selected')\\}</tt>\n </p>\n \n <p class=\"m-t-16\">\n <b>Asset properties:</b> Use \"Asset properties\" to select other properties (incl. computed ones) and access their values via <code>c8yProperties</code>: <br>\n <i>Last updated:</i> <tt>\\${this.c8yProperties?.lastUpdated || '-'}</tt> <br>\n <i>Last device message:</i> <tt>\\${this.c8yProperties?.lastDeviceMessage || '-'}</tt>\n </p>\n\n <p class=\"m-t-16\">\n <b>Styles:</b> Use the CSS editor to customize styles. You can use <span class=\"text-bold\">any design-token CSS variable</span> in there.\n </p>\n\n <p class=\"m-t-16\">\n <b>Translations:</b> Use the <code>c8yTranslate('text {{ var }}', { var: value })</code> function to translate strings.\n <i>Note: texts must be written in English and their translations must be available in the loaded standard or custom translation resources.</i>\n </p>\n\n <p class=\"m-t-16\">\n <b>Buttons:</b> Use other HTML elements like buttons: <br>\n <a class=\"btn btn-primary m-t-8\" href=\"#/group\">\\$\\{this.c8yTranslate('Go to groups')\\}</a>\n </p>\n</div>`;\nexport const INITIAL_CSS_FORMATTED = `\n:host > div {\n padding: var(--c8y-root-component-padding-default);\n}\nspan.branded { \n color: var(--brand-primary, var(--c8y-brand-primary)); \n}`;\n\nexport const defaultWebComponentName = 'DefaultWebComponent';\n\nexport const defaultWebComponentAttributeNameContext = 'c8yContext';\n","import { defaultWebComponentName } from './html-widget.model';\n\nexport const webComponentTemplate = (\n html: string,\n css?: string,\n viewEncapsulation?: boolean,\n name = defaultWebComponentName\n) => `\nimport { LitElement, html, css} from 'lit';\n${!viewEncapsulation ? `import { styleImports } from 'styles';` : ''}\n\nexport default class ${name} extends LitElement {\n static styles = css\\`\n ${css}\n \\`;\n\n static properties = {\n // The managed object this widget is assigned to. Can be null.\n c8yContext: { type: Object },\n // The object with realtime values of configured properties. Can be null.\n c8yProperties: { type: Object },\n // The instant translation function.\n c8yTranslate: { type: Function },\n };\n\n constructor() {\n super();\n }\n\n render() {\n return html\\`${\n viewEncapsulation\n ? html\n : `\n <style>\n \\${styleImports}\n </style>\n ${html}\n `\n }\\`;\n }\n}\n`;\n","export const legacyTemplate = (html: string, deviceId?: string | number, deviceName?: string) => {\n const escapedHtml = html\n // Finds: \\ Replaces with: \\\\\n .replace(/\\\\/g, '\\\\\\\\')\n // Finds: ` Replaces with: \\`\n .replace(/`/g, '\\\\`')\n // Finds: $ Replaces with: \\$\n .replace(/\\$/g, '\\\\$');\n\n return `\nimport { angular } from 'angular';\n\n// NOTE: This is a legacy template for the HTML widget.\n// It is used to compile the HTML content in the context of the AngularJS application.\n// The template is injected into the AngularJS application and compiled using the AngularJS compiler.\n// The template should only be used for backward compatibility purposes.\n// It is recommended to use a web component instead.\n\nif(!angular) {\n throw new Error('AngularJS is not available. Please make sure to include AngularJS in your project.');\n}\n\nconst $injector = angular.element(document.querySelector('c8y-ui-root')).injector();\nif (!$injector) {\n throw new Error('AngularJS injector is not available. Maybe not an hybrid application?');\n}\n\n// defining a new scope\nconst $rootScope = $injector.get('$rootScope');\nconst $scope = $rootScope.$new(true); \n\n// faking the old angularjs config \n$scope.child = { \n config: {\n ${deviceId ? `device: { id: \"${deviceId}\", name: \"${deviceName}\" },` : ''}\n html: \\`<div ng-controller=\"HtmlWidgetCtrl\">${escapedHtml}</div>\\`\n }\n};\n\n// load the needed services\nconst $compile = $injector.get('$compile');\nconst $controller = $injector.get('$controller');\n\n// create the element\nconst htmlElement = angular.element($scope.child.config.html);\n\n// The default controller providing the context\n$controller('HtmlWidgetCtrl', { $scope });\n\n// Compile the element\n$compile(htmlElement)($scope);\n\n// Apply the scope changes\n$rootScope.$apply();\n\nexport default htmlElement[0];`;\n};\n","import { inject, Injectable } from '@angular/core';\nimport { AppStateService } from '@c8y/ngx-components';\nimport { CockpitConfig } from '@c8y/ngx-components/cockpit-config';\nimport {\n ContextWidgetConfig,\n WidgetConfigNotification,\n WidgetConfigService\n} from '@c8y/ngx-components/context-dashboard';\nimport { isEmpty } from 'lodash';\nimport {\n combineLatest,\n debounceTime,\n distinctUntilChanged,\n filter,\n first,\n map,\n Observable,\n of,\n shareReplay,\n startWith,\n Subject,\n switchMap,\n takeUntil,\n withLatestFrom\n} from 'rxjs';\nimport { HtmlWidget, INITIAL_CSS_FORMATTED, INITIAL_HTML_FORMATTED } from './html-widget.model';\nimport { webComponentTemplate } from './webcomponent-template';\nimport { legacyTemplate } from './legacy-template';\n\n@Injectable()\nexport class HtmlWidgetConfigService {\n readonly DEFAULT_AUTO_SAVE_DEBOUNCE = 1000;\n codeChange$ = new Subject<{ value: string; type: 'css' | 'code' }>();\n widgetConfigService = inject(WidgetConfigService);\n appState = inject(AppStateService);\n destroy$ = new Subject<void>();\n readonly notify$: Observable<WidgetConfigNotification> = this.widgetConfigService.notify$;\n\n init$ = this.widgetConfigService.currentConfig$.pipe(\n first(),\n map(current => {\n if (current.html) {\n current.config = this.mapLegacyConfig(current);\n }\n\n return (current.config || {}) as HtmlWidget;\n }),\n filter(config => !!config),\n withLatestFrom(this.appState.currentApplicationConfig),\n switchMap(([widgetConfig, appConfig]) => this.initConfig(appConfig, widgetConfig)),\n shareReplay(),\n takeUntil(this.destroy$)\n );\n\n config$ = this.init$.pipe(switchMap(initValue => this.configChanged$.pipe(startWith(initValue))));\n\n codeEditorChangeConfig$ = combineLatest([\n this.codeChange$.pipe(startWith(undefined)),\n this.config$\n ]).pipe(\n distinctUntilChanged(),\n takeUntil(this.destroy$),\n debounceTime(this.DEFAULT_AUTO_SAVE_DEBOUNCE),\n map(([change, config]) => {\n if (!change) {\n return config;\n }\n if (change.type === 'css') {\n config.css = change.value;\n } else {\n config.code = change.value;\n }\n return { ...config };\n })\n );\n\n configChanged$ = new Subject<HtmlWidget>();\n\n initConfig(appConfig: CockpitConfig, widgetConfig: HtmlWidget): Observable<HtmlWidget> {\n const defaultToAdvancedMode = appConfig?.htmlWidgetDefaultToAdvancedMode ?? false;\n const isEmptyConfig = isEmpty(widgetConfig);\n if (isEmptyConfig && !defaultToAdvancedMode) {\n widgetConfig = this.initDefaultMode(!appConfig?.htmlWidgetDisableSanitization);\n this.save(widgetConfig);\n return of(widgetConfig);\n }\n if (isEmptyConfig) {\n widgetConfig = this.enableAdvancedMode(widgetConfig);\n }\n\n // new config is needed to trigger ngOnChanges\n const newConfig = { ...widgetConfig };\n this.save(newConfig);\n return of(newConfig);\n }\n\n destroy(): void {\n this.destroy$.next();\n this.codeChange$.complete();\n this.configChanged$.complete();\n }\n\n save(config: HtmlWidget) {\n this.widgetConfigService.updateConfig({\n config\n });\n }\n\n changeCode(value: string) {\n this.codeChange$.next({ value, type: 'code' });\n }\n\n changeCss(value: string) {\n this.codeChange$.next({ value, type: 'css' });\n }\n\n enableAdvancedMode(currentConfig: HtmlWidget) {\n const currentHTML = currentConfig?.code || INITIAL_HTML_FORMATTED;\n const currentCSS = currentConfig?.css || INITIAL_CSS_FORMATTED;\n const code = currentConfig?.legacy\n ? legacyTemplate(\n currentHTML,\n this.widgetConfigService.currentConfig?.device?.id,\n this.widgetConfigService.currentConfig?.device?.name\n )\n : webComponentTemplate(currentHTML, currentCSS, false);\n currentConfig = {\n css: '',\n code,\n legacy: false,\n devMode: true,\n options: {\n cssEncapsulation: false,\n advancedSecurity: false\n }\n };\n return currentConfig;\n }\n\n initDefaultMode(advancedSecurity = true): HtmlWidget {\n return {\n css: INITIAL_CSS_FORMATTED,\n code: INITIAL_HTML_FORMATTED,\n legacy: false,\n devMode: false,\n options: {\n cssEncapsulation: false,\n advancedSecurity\n }\n };\n }\n\n private mapLegacyConfig(current: ContextWidgetConfig): HtmlWidget {\n const isAlreadyInAdvancedMode = current?.config?.devMode === true;\n if (isAlreadyInAdvancedMode) {\n return current.config as HtmlWidget;\n }\n\n const isAlreadyMapped = current?.config?.legacy === true;\n if (isAlreadyMapped) {\n return current.config as HtmlWidget;\n }\n\n return {\n code: current.html,\n css: '',\n legacy: true,\n devMode: false,\n options: {\n cssEncapsulation: false,\n advancedSecurity: current.sanitization === 'strict'\n }\n };\n }\n}\n","import { NgIf } from '@angular/common';\nimport { Component, inject, Input } from '@angular/core';\nimport { FormsModule } from '@angular/forms';\nimport { Router } from '@angular/router';\nimport { gettext } from '@c8y/ngx-components/gettext';\nimport { C8yTranslatePipe, IconDirective, Permissions } from '@c8y/ngx-components';\nimport { WidgetConfigService } from '@c8y/ngx-components/context-dashboard';\nimport { PopoverModule } from 'ngx-bootstrap/popover';\nimport { TooltipModule } from 'ngx-bootstrap/tooltip';\nimport { HtmlWidgetConfigService } from '../html-widget-config.service';\nimport { HtmlWidgetConfig, HtmlWidgetOptions } from '../html-widget.model';\n\n@Component({\n standalone: true,\n imports: [IconDirective, NgIf, TooltipModule, PopoverModule, C8yTranslatePipe, FormsModule],\n selector: 'c8y-html-widget-advanced-settings',\n templateUrl: './advanced-settings.component.html'\n})\nexport class AdvancedSettingsComponent {\n widgetConfigService = inject(WidgetConfigService);\n htmlWidgetConfigService = inject(HtmlWidgetConfigService);\n router = inject(Router);\n permissionService = inject(Permissions);\n\n canChangeSettings = false;\n @Input()\n devMode: boolean;\n @Input()\n cssEncapsulation: boolean;\n\n CSS_ENCAPSULATION_HELP_CONTEXT = gettext(\n 'If enabled, the CSS will be encapsulated and no platform styling will be applied.'\n );\n\n ngOnInit(): void {\n this.canChangeSettings = this.permissionService.hasAnyRole([\n Permissions.ROLE_APPLICATION_MANAGEMENT_ADMIN,\n Permissions.ROLE_TENANT_ADMIN\n ]);\n }\n\n disableAdvancedMode() {\n const config = this.htmlWidgetConfigService.initDefaultMode();\n this.htmlWidgetConfigService.save(config);\n this.htmlWidgetConfigService.configChanged$.next(config);\n }\n\n enableAdvancedMode() {\n let { config } = this.widgetConfigService.currentConfig as HtmlWidgetConfig;\n config = this.htmlWidgetConfigService.enableAdvancedMode(config);\n this.htmlWidgetConfigService.save(config);\n this.htmlWidgetConfigService.configChanged$.next(config);\n }\n\n toggleAdvancedMode() {\n this.devMode ? this.disableAdvancedMode() : this.enableAdvancedMode();\n this.devMode = !this.devMode;\n }\n\n async changeOption(option: keyof HtmlWidgetOptions) {\n const { config } = this.widgetConfigService.currentConfig as HtmlWidgetConfig;\n config.options[option] = !config.options[option];\n this.htmlWidgetConfigService.save(config);\n this.htmlWidgetConfigService.configChanged$.next(config);\n }\n}\n","<fieldset class=\"c8y-fieldset m-t-0\">\n <legend>{{ 'Developer mode' | translate }}</legend>\n\n <div class=\"d-flex a-i-center p-b-16\">\n <label class=\"c8y-switch\">\n <input\n type=\"checkbox\"\n [ngModel]=\"devMode\"\n (change)=\"toggleAdvancedMode()\"\n [disabled]=\"!canChangeSettings\"\n />\n <span></span>\n <span>{{ 'Advanced developer mode' | translate }}</span>\n </label>\n\n <button class=\"btn-help\"\n [attr.aria-label]=\"'Help' | translate\"\n [popover]=\"devMode ? disableAdvanced : enableAdvanced\"\n container=\"body\"\n placement=\"right\"\n triggers=\"focus\"\n type=\"button\"\n ></button>\n\n <ng-template #enableAdvanced>\n <p class=\"text-16 text-bold p-b-8\">\n <i [c8yIcon]=\"'imac-settings'\"></i>\n {{ 'Advanced developer mode' | translate }}\n </p>\n <p class=\"p-b-8\" translate>\n Create custom widgets by modifying a basic WebComponent with HTML and JavaScript. This\n <strong>unsupported</strong>\n feature is ideal for rapid prototyping and simple customizations.\n </p>\n <p class=\"p-b-8\" translate>\n For production environments, we recommend our fully-supported Angular-based\n <a href=\"https://styleguide.cumulocity.com\" target=\"_blank\">Web SDK</a>.\n <br />\n Enable advanced developer mode to start coding!\n </p>\n </ng-template>\n\n <ng-template #disableAdvanced>\n <p class=\"text-16 text-bold p-b-8\">\n {{ 'Advanced developer mode' | translate }}\n </p>\n <p class=\"p-b-8\" translate>\n The advanced developer mode is enabled for this widget allowing to build extensive Web\n Components.\n </p>\n <p class=\"p-b-8\" translate>\n You can disable this mode again, but it will reset the current code.\n </p>\n </ng-template>\n\n <ng-container *ngIf=\"!devMode\">\n <label\n class=\"c8y-switch m-l-auto\"\n >\n <input\n type=\"checkbox\"\n (change)=\"changeOption('cssEncapsulation')\"\n [disabled]=\"!canChangeSettings\"\n [ngModel]=\"cssEncapsulation\"\n />\n <span></span>\n <span>{{ 'CSS encapsulation' | translate }}</span>\n </label>\n <button\n class=\"btn-help m-0\"\n [attr.aria-label]=\"'Help' | translate\"\n popover=\"{{ CSS_ENCAPSULATION_HELP_CONTEXT | translate }}\"\n triggers=\"focus\"\n placement=\"left\"\n container=\"body\"\n type=\"button\"\n ></button>\n </ng-container>\n\n </div>\n</fieldset>\n","import { ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core';\nimport { C8yTranslatePipe, IconDirective } from '@c8y/ngx-components';\nimport { ToolCallPart } from '@c8y/ngx-components/ai';\nimport { TooltipDirective } from 'ngx-bootstrap/tooltip';\nimport { HtmlWidgetConfigService } from './html-widget-config.service';\n\n@Component({\n selector: 'c8y-html-ai-chat-tool-details',\n templateUrl: './html-ai-chat-tool-details.component.html',\n imports: [TooltipDirective, C8yTranslatePipe, IconDirective],\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush\n})\nexport class HtmlAiChatToolDetailsComponent {\n tool = input.required<ToolCallPart<{ code: string }>>();\n code = computed(() => this.tool().input?.code || '');\n isExecuting = computed(() => this.tool().type !== 'tool-result');\n\n private readonly htmlWidgetConfigService = inject(HtmlWidgetConfigService);\n\n /**\n * Revert to the last applied code.\n */\n revert() {\n this.applyCurrentCode();\n }\n\n private applyCurrentCode() {\n const newConfig = {\n code: this.code(),\n css: '',\n devMode: true,\n legacy: false,\n options: { advancedSecurity: false, cssEncapsulation: false }\n };\n this.htmlWidgetConfigService.configChanged$.next(newConfig);\n this.htmlWidgetConfigService.widgetConfigService.updateConfig({ config: newConfig });\n }\n}\n","<pre\n class=\"fit-w\"\n style=\"max-height: 320px\"\n >{{ code() }}</pre\n>\n@if (!isExecuting()) {\n <button\n class=\"btn btn-default btn-sm\"\n [attr.aria-label]=\"'Revert to this version' | translate\"\n [tooltip]=\"'Revert to this version' | translate\"\n container=\"body\"\n (click)=\"revert()\"\n >\n <i c8yIcon=\"undo\"></i>\n </button>\n}\n","import { NgClass } from '@angular/common';\nimport {\n Component,\n ElementRef,\n inject,\n Input,\n OnChanges,\n OnDestroy,\n SecurityContext,\n SimpleChanges,\n viewChild\n} from '@angular/core';\nimport { DomSanitizer } from '@angular/platform-browser';\nimport { IIdentified, IManagedObject, InventoryService } from '@c8y/client';\nimport { Alert, ManagedObjectRealtimeService } from '@c8y/ngx-components';\nimport { kebabCase } from 'lodash-es';\nimport {\n catchError,\n EMPTY,\n filter,\n from,\n fromEvent,\n isEmpty,\n map,\n merge,\n Observable,\n Subject,\n switchMap,\n takeUntil\n} from 'rxjs';\nimport { defaultWebComponentName, HtmlWidget, WebcomponentContext } from '../html-widget.model';\nimport { webComponentTemplate } from '../webcomponent-template';\nimport { legacyTemplate } from '../legacy-template';\nimport { TranslateService } from '@ngx-translate/core';\n\n@Component({\n standalone: true,\n imports: [NgClass],\n selector: 'c8y-html-frame',\n templateUrl: './html-frame.component.html',\n host: { class: 'd-contents' }\n})\nexport class HtmlFrameComponent implements OnChanges, OnDestroy {\n @Input()\n config: HtmlWidget;\n\n @Input()\n device: IManagedObject | IIdentified;\n\n @Input()\n propertyValues$: Observable<Record<string, unknown>> = EMPTY;\n\n /**\n * If set to true, it will be ensured that a unique hash is generated\n * for every webcomponent. This is useful if configured as otherwise it might\n * happen that the same code is already used in another webcomponent and the\n * error messages can not be assigned correctly.\n */\n @Input()\n useSalt = false;\n\n alerts: Alert[] = [];\n\n private sanitizer = inject(DomSanitizer);\n private translateService = inject(TranslateService);\n private moRealtime = inject(ManagedObjectRealtimeService);\n private destroy$ = new Subject<void>();\n private hostElement = viewChild<ElementRef<HTMLDivElement>>('hostElement');\n private reload$ = new Subject<void>();\n private latestUrl?: string;\n private htmlContentInitialization$ = this.reload$.pipe(\n filter(() => !!this.hostElement()),\n map(() => this.hostElement().nativeElement),\n switchMap((div: HTMLDivElement) =>\n merge(\n this.listenToErrors(),\n from(this.initDiv(div)).pipe(\n isEmpty(),\n filter(isEmpty => !!isEmpty),\n catchError(error => from([{ text: error, type: 'danger' }]))\n )\n )\n ),\n filter(alert => !!alert),\n takeUntil(this.destroy$)\n );\n private inventoryService = inject(InventoryService);\n\n constructor() {\n this.htmlContentInitialization$.pipe(takeUntil(this.destroy$)).subscribe((alert: Alert) => {\n this.alerts.push(alert);\n console.error(alert.text);\n });\n }\n\n ngOnDestroy(): void {\n this.destroy$.next();\n this.destroy$.complete();\n }\n\n ngOnChanges(changes: SimpleChanges): void {\n if (\n changes.config?.currentValue ||\n (this.config && changes.device?.previousValue !== changes.device?.currentValue)\n ) {\n this.reloadComponent();\n }\n }\n\n reloadComponent() {\n this.alerts = [];\n const div = this.hostElement();\n div.nativeElement.innerHTML = '';\n this.reload$.next();\n }\n\n async initDiv(divHostElement: HTMLDivElement) {\n const code = this.getCode();\n const hash = await this.generateHash(code, this.useSalt);\n const webComponentName = kebabCase(defaultWebComponentName) + hash;\n const context: IManagedObject = await this.getContext(this.device);\n\n if (customElements.get(webComponentName)) {\n return this.createWebComponent(webComponentName, divHostElement, context);\n }\n\n const url = this.generateUrl(code);\n const defaultModule = await this.loadScript(url);\n\n // if the default module is a string, we will not use a webcomponent\n // instead we will simply parse the string and add it to the div\n // this is the case for legacy HTML widgets\n if (typeof defaultModule.default === 'string') {\n divHostElement.innerHTML = defaultModule.default;\n return EMPTY;\n }\n\n // same goes for an HTML Element\n if (defaultModule.default instanceof HTMLElement) {\n divHostElement.appendChild(defaultModule.default);\n\n // Find and execute scripts\n const scripts = divHostElement.querySelectorAll('script');\n scripts.forEach(script => {\n const newScript = document.createElement('script');\n Array.from(script.attributes).forEach(attr => {\n newScript.setAttribute(attr.name, attr.value);\n });\n newScript.textContent = script.textContent;\n script.parentNode.replaceChild(newScript, script);\n });\n return EMPTY;\n }\n\n // as a race condition can happen on loading, we need\n // to check again if the web component is already defined\n if (!customElements.get(webComponentName)) {\n customElements.define(webComponentName, defaultModule.default);\n }\n this.createWebComponent(webComponentName, divHostElement, context);\n return EMPTY;\n }\n\n private async getContext(\n device: IManagedObject | IIdentified | undefined\n ): Promise<IManagedObject> {\n if (!device) {\n return;\n }\n\n if (!device.self) {\n const { data } = await this.inventoryService.detail(device.id);\n return data;\n }\n return device as IManagedObject;\n }\n\n private async loadScript(url: string): Promise<{ default: any }> {\n const module = await import(/* webpackIgnore: true */ url);\n if (!module.default) {\n throw 'No default export found. Add an \"export default\" statement to your code.';\n }\n\n return module;\n }\n\n private generateUrl(script: string): string {\n const blob = new Blob([script], { type: 'application/javascript' });\n const url = URL.createObjectURL(blob);\n if (this.latestUrl) {\n URL.revokeObjectURL(this.latestUrl);\n }\n this.latestUrl = url;\n return url;\n }\n\n private listenToErrors(): Observable<Alert> {\n const errorEvents$ = fromEvent<ErrorEvent>(window, 'error').pipe(\n filter(event => event.filename?.includes(this.latestUrl)),\n map(event => this.mapErrorEventToAlert(event))\n );\n\n const rejectionEvents$ = fromEvent<PromiseRejectionEvent>(window, 'unhandledrejection').pipe(\n filter(event => event.reason?.stack?.includes(this.latestUrl)),\n map(event => this.mapErrorEventToAlert(event)),\n takeUntil(this.destroy$)\n );\n\n return merge(errorEvents$, rejectionEvents$);\n }\n\n private createWebComponent(\n webComponentName: string,\n divHostElement: HTMLDivElement,\n context: IManagedObject\n ) {\n const webComponent: WebcomponentContext = document.createElement<any>(webComponentName);\n const instantTranslate = this.translateService.instant.bind(this.translateService);\n webComponent.c8yTranslate = instantTranslate;\n\n // TODO: should c8yContext be updated in realtime and also connected under global time controls?\n webComponent.c8yContext = context;\n if (context?.id) {\n this.moRealtime\n .onAll$(context.id)\n .pipe(takeUntil(merge(this.reload$, this.destroy$)))\n .subscribe(res => {\n webComponent.c8yContext = res.data as IManagedObject;\n });\n }\n this.propertyValues$.pipe(takeUntil(merge(this.reload$, this.destroy$))).subscribe(values => {\n webComponent.c8yProperties = values;\n });\n divHostElement.appendChild(webComponent);\n return webComponent;\n }\n\n private mapErrorEventToAlert(event: PromiseRejectionEvent | ErrorEvent): Alert {\n const hasReason = 'reason' in event;\n if (hasReason && event.reason?.name === ReferenceError.name) {\n const undefinedVar = event.reason.message.split(' ')[0];\n return { text: undefinedVar + ' is not defined', type: 'info' };\n }\n return { text: hasReason ? event.reason.message : event.message, type: 'danger' };\n }\n\n private getCode() {\n const isDevMode = this.config.devMode;\n if (isDevMode) {\n return this.config.code;\n }\n return this.createDefaultWebcomponentCode();\n }\n\n private async generateHash(value: string, useSalt: boolean): Promise<string> {\n const encoder = new TextEncoder();\n const data = encoder.encode(value + (useSalt ? Math.random() : ''));\n const hashBuffer = await crypto.subtle.digest('SHA-256', data);\n const hashArray = Array.from(new Uint8Array(hashBuffer));\n const hashHex = hashArray.map(byte => byte.toString(16).padStart(2, '0')).join('');\n return hashHex;\n }\n\n private createDefaultWebcomponentCode(): string {\n if (this.config.legacy) {\n const legacyWebComponent = legacyTemplate(\n this.config.options.advancedSecurity\n ? this.sanitizer.sanitize(SecurityContext.HTML, this.config.code)\n : this.config.code,\n this.device?.id,\n this.device?.name\n );\n return legacyWebComponent;\n }\n\n const webComponentScript = webComponentTemplate(\n this.config.options.advancedSecurity\n ? this.sanitizer.sanitize(SecurityContext.HTML, this.config.code)\n : this.config.code,\n this.config.options.advancedSecurity\n ? this.sanitizer.sanitize(SecurityContext.STYLE, this.config.css)\n : this.config.css,\n this.config.options.cssEncapsulation\n );\n return webComponentScript;\n }\n}\n","@for (alert of alerts; track alert) {\n <div\n class=\"alert m-8\"\n role=\"alert\"\n [ngClass]=\"{\n 'alert-danger': alert.type === 'danger',\n 'alert-warning': alert.type === 'warning',\n 'alert-info': alert.type === 'info',\n 'alert-success': alert.type === 'success'\n }\"\n >\n <p><strong translate>There was an issue in the HTML widget:</strong></p>\n <pre>{{ alert.text }}</pre>\n </div>\n}\n<div\n class=\"fit-w fit-h\"\n #hostElement\n></div>\n","import { AsyncPipe } from '@angular/common';\nimport { Component, inject, Input, OnDestroy, SimpleChanges, ViewChild } from '@angular/core';\nimport { FormsModule } from '@angular/forms';\nimport { C8yTranslatePipe, IconDirective, LoadingComponent, TabsModule } from '@c8y/ngx-components';\nimport { TooltipModule } from 'ngx-bootstrap/tooltip';\nimport { PopoverModule } from 'ngx-bootstrap/popover';\nimport { BsDropdownModule } from 'ngx-bootstrap/dropdown';\nimport {\n WidgetConfigFeedbackComponent,\n WidgetConfigService,\n quoteAndEscape\n} from '@c8y/ngx-components/context-dashboard';\nimport { EditorComponent } from '@c8y/ngx-components/editor';\nimport type * as Monaco from 'monaco-editor';\nimport { map, Subject, takeUntil } from 'rxjs';\nimport { HtmlWidgetConfigService } from '../html-widget-config.service';\nimport { HtmlWidget } from '../html-widget.model';\nimport { AdvancedSettingsComponent } from '../advanced-settings/advanced-settings.component';\nimport { gettext } from '@c8y/ngx-components/gettext';\nimport { TranslateService } from '@ngx-translate/core';\n\n@Component({\n standalone: true,\n imports: [\n AsyncPipe,\n EditorComponent,\n FormsModule,\n IconDirective,\n C8yTranslatePipe,\n WidgetConfigFeedbackComponent,\n TabsModule,\n TooltipModule,\n PopoverModule,\n BsDropdownModule,\n LoadingComponent,\n AdvancedSettingsComponent\n ],\n selector: 'c8y-widget-code-editor',\n templateUrl: './widget-code-editor.component.html'\n})\nexport class WidgetCodeEditorComponent implements OnDestroy {\n @Input()\n mode: 'code' | 'css' = 'code';\n\n @Input()\n config: HtmlWidget;\n\n @ViewChild(EditorComponent) editorComponent!: EditorComponent;\n\n configService = inject(HtmlWidgetConfigService);\n translate = inject(TranslateService);\n private widgetConfigService = inject(WidgetConfigService);\n\n propertyKeys$ = this.widgetConfigService.currentConfig$.pipe(\n map(config => Object.keys(config?.properties || {}).sort((a, b) => a.localeCompare(b)))\n );\n\n editor: Monaco.editor.IStandaloneCodeEditor;\n isAutoSaveEnabled = true;\n language: 'html' | 'css' | 'javascript' = 'html';\n value: string;\n isLoading = false;\n\n readonly TAB_WEBCOMPONENT_LABEL = gettext('Web Component`Tab label of HTML Widget`');\n readonly TAB_HTML_LABEL = gettext('HTML`Tab label of HTML Widget`');\n readonly TAB_CSS_LABEL = gettext('CSS`Tab label of HTML Widget`');\n readonly BUTTON_DISABLE_AUTOSAVE_LABEL = gettext(\n 'Disable auto save`An action you can do on the html widget editor`'\n );\n readonly BUTTON_ENABLE_AUTOSAVE_LABEL = gettext(\n 'Enable auto save`An action you can do on the html widget editor`'\n );\n readonly TAB_OUTLET_NAME = 'html-widget-tab-outlet';\n\n private destroy$ = new Subject<void>();\n private suggestionProviders: Monaco.IDisposable[] = [];\n\n ngOnDestroy(): void {\n this.suggestionProviders.forEach(a => a.dispose());\n this.destroy$.next();\n this.destroy$.complete();\n }\n\n ngOnChanges(changes: SimpleChanges): void {\n if (changes.config) {\n this.changeCode(this.config.code);\n }\n\n if (changes.config?.currentValue) {\n this.loadCode();\n }\n }\n\n loadCode() {\n this.isLoading = true;\n const isInDevMode = this.config?.devMode;\n this.language = 'html';\n\n if (isInDevMode) {\n this.language = 'javascript';\n }\n\n if (this.mode === 'css') {\n this.language = 'css';\n }\n\n this.value = this.mode === 'code' ? this.config.code : this.config.css;\n\n this.isLoading = false;\n\n if (this.editor) {\n queueMicrotask(() => this.formatCode());\n }\n }\n\n switchMode(mode: 'code' | 'css') {\n this.mode = mode;\n this.loadCode();\n }\n\n editorLoaded(editor: Monaco.editor.IStandaloneCodeEditor) {\n this.editor = editor;\n const monaco = this.editorComponent.monaco;\n this.editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {\n this.saveCode();\n });\n\n this.propertyKeys$.pipe(takeUntil(this.destroy$)).subscribe(keys => {\n this.registerPropertySuggestions(keys);\n });\n }\n\n formatCode() {\n this.editor.getAction('editor.action.formatDocument').run();\n }\n\n redo() {\n this.editor.trigger('keyboard', 'redo', null);\n }\n\n undo() {\n this.editor.trigger('keyboard', 'undo', null);\n }\n\n changeCode($event: string) {\n if (this.isAutoSaveEnabled) {\n this.saveCode($event);\n }\n }\n\n saveCode(codeStr?: string) {\n const code = codeStr || this.editor.getValue();\n if (this.mode === 'code') {\n this.configService.changeCode(code);\n return;\n }\n this.configService.changeCss(code);\n }\n\n insertPropertyAtCursor(key: string) {\n if (!this.editor) {\n return;\n }\n const text = this.getFormattedPropertyExpression(key);\n\n const monaco = this.editorComponent.monaco;\n const position = this.editor.getPosition();\n const range = new monaco.Range(\n position.lineNumber,\n position.column,\n position.lineNumber,\n position.column\n );\n\n this.editor.executeEdits('insert-property', [{ range, text, forceMoveMarkers: true }]);\n this.editor.focus();\n }\n\n private registerPropertySuggestions(keys: string[]) {\n this.suggestionProviders.forEach(a => a.dispose());\n this.suggestionProviders = [];\n\n if (!keys.length) return;\n\n const monaco = this.editorComponent.monaco;\n\n const providerOptions: Monaco.languages.CompletionItemProvider = {\n triggerCharacters: ['$', '.', '[', \"'\"],\n provideCompletionItems: (model, position) => {\n const lineText = model.getValueInRange({\n startLineNumber: position.lineNumber,\n startColumn: 1,\n endLineNumber: position.lineNumber,\n endColumn: position.column\n });\n\n const makeRange = (startColumn: number): Monaco.IRange => ({\n startLineNumber: position.lineNumber,\n endLineNumber: position.lineNumber,\n startColumn,\n endColumn: position.column\n });\n\n // Case 1: after dot access – this.c8yProperties?. or this.c8yProperties.\n const dotMatch = /this\\.c8yProperties\\??\\.(\\w*)$/.exec(lineText);\n if (dotMatch) {\n const typed = dotMatch[1];\n const startColumn = position.column - typed.length;\n const suggestions: Monaco.languages.CompletionItem[] = keys\n .filter(key => !/[^a-zA-Z0-9_]/.test(key) && key.startsWith(typed))\n .map(key => ({\n label: key,\n kind: monaco.languages.CompletionItemKind.Property,\n insertText: key,\n range: makeRange(startColumn),\n sortText: key\n }));\n return { suggestions };\n }\n\n // Case 2: after bracket access – this.c8yProperties?.[ or this.c8yProperties[\n const bracketMatch = /this\\.c8yProperties(?:\\?\\.)?\\[([^\\]]*)$/.exec(lineText);\n if (bracketMatch) {\n const typed = bracketMatch[1];\n const charAfterCursor = model.getValueInRange({\n startLineNumber: position.lineNumber,\n startColumn: position.column,\n endLineNumber: position.lineNumber,\n endColumn: position.column + 1\n });\n const closingBracketAlreadyPresent = charAfterCursor === ']';\n const startColumn = position.column - typed.length;\n const endColumn = closingBracketAlreadyPresent ? position.column + 1 : position.column;\n const suggestions: Monaco.languages.CompletionItem[] = keys.map(key => ({\n label: key,\n kind: monaco.languages.CompletionItemKind.Property,\n insertText: `${quoteAndEscape(key)}]`,\n range: {\n startLineNumber: position.lineNumber,\n endLineNumber: position.lineNumber,\n startColumn,\n endColumn\n },\n sortText: key\n }));\n return { suggestions };\n }\n\n // Case 3: fresh new expression – cursor is at a word boundary and the user\n // has started typing a key name.\n const freshMatch = /(?:^|[\\s=(<>,;{}\\[\\]\"'`])(\\w*)$/.exec(lineText);\n if (freshMatch) {\n const typed = freshMatch[1];\n if (!typed) return { suggestions: [] };\n const startColumn = position.column - typed.length;\n const suggestions: Monaco.languages.CompletionItem[] = keys\n .filter(key => key.startsWith(typed))\n .map(key => {\n const insertText = this.getFormattedPropertyExpression(key);\n return {\n label: key,\n kind: monaco.languages.CompletionItemKind.Property,\n insertText,\n range: makeRange(startColumn),\n detail: insertText,\n filterText: key,\n sortText: key\n };\n });\n return { suggestions };\n }\n\n return { suggestions: [] };\n }\n };\n\n this.suggestionProviders.push(\n monaco.languages.registerCompletionItemProvider('html', providerOptions),\n monaco.languages.registerCompletionItemProvider('javascript', providerOptions)\n );\n }\n\n private getFormattedPropertyExpression(key: string): string {\n const nonAlphanumericRegex = /[^a-zA-Z0-9]/;\n const formattedKey = nonAlphanumericRegex.test(key) ? `[${quoteAndEscape(key)}]` : `${key}`;\n return '${this.c8yProperties?.' + formattedKey + '}';\n }\n}\n","<c8y-widget-config-feedback>\n <div class=\"d-flex\">\n @if (config?.devMode && !config?.legacy) {\n <span\n class=\"tag tag--warning text-12\"\n translate\n >\n Advanced developer mode\n </span>\n }\n </div>\n <div class=\"d-flex\">\n @if (config?.legacy) {\n <span\n class=\"tag tag--warning text-12\"\n [title]=\"\n 'This widget is in legacy mode. Consider to upgrade this to a new HTML widget. Read our documentation on details to transform your widget'\n | translate\n \"\n translate\n >\n Legacy mode\n </span>\n }\n </div>\n</c8y-widget-config-feedback>\n\n<div class=\"d-flex d-col fit-h fit-w\">\n <c8y-html-widget-advanced-settings\n [devMode]=\"config?.devMode\"\n [cssEncapsulation]=\"config?.options?.cssEncapsulation\"\n ></c8y-html-widget-advanced-settings>\n\n <fieldset class=\"c8y-fieldset p-0 overflow-hidden\">\n <legend class=\"m-l-16 p-l-0\">{{ 'Code' | translate }}</legend>\n\n <div class=\"btn-group btn-group-sm m-l-0 p-t-8 p-b-8 p-l-16 p-r-16 fit-w d-flex\">\n <button\n class=\"btn btn-default\"\n [attr.aria-label]=\"'Undo' | translate\"\n [tooltip]=\"'Undo' | translate\"\n placement=\"top\"\n container=\"body\"\n type=\"button\"\n [delay]=\"500\"\n (click)=\"undo()\"\n >\n <i [c8yIcon]=\"'undo'\"></i>\n </button>\n\n <button\n class=\"btn btn-default\"\n [attr.aria-label]=\"'Redo' | translate\"\n [tooltip]=\"'Redo' | translate\"\n placement=\"top\"\n container=\"body\"\n type=\"button\"\n [delay]=\"500\"\n (click)=\"redo()\"\n >\n <i [c8yIcon]=\"'redo'\"></i>\n </button>\n\n <button\n class=\"btn btn-default\"\n [attr.aria-label]=\"'Format code' | translate\"\n [tooltip]=\"'Format code' | translate\"\n placement=\"top\"\n container=\"body\"\n type=\"button\"\n [delay]=\"500\"\n (click)=\"formatCode()\"\n >\n <i [c8yIcon]=\"'format-align-left'\"></i>\n </button>\n\n @let propertyKeys = propertyKeys$ | async;\n @if (mode !== 'css' && propertyKeys?.length) {\n <div\n class=\"btn-group btn-group-sm m-l-4\"\n container=\"body\"\n dropdown\n >\n <button\n class=\"btn btn-default\"\n [attr.aria-label]=\"'Insert property' | translate\"\n [tooltip]=\"'Insert property' | translate\"\n placement=\"top\"\n container=\"body\"\n type=\"button\"\n [delay]=\"500\"\n dropdownToggle\n >\n <i [c8yIcon]=\"'plus-circle'\"></i>\n </button>\n <ul\n class=\"dropdown-menu\"\n *dropdownMenu\n >\n @for (key of propertyKeys; track key) {\n <li>\n <button\n class=\"dropdown-item\"\n type=\"button\"\n (click)=\"insertPropertyAtCursor(key)\"\n >\n {{ key }}\n </button>\n </li>\n }\n </ul>\n </div>\n }\n\n <label class=\"c8y-switch m-l-auto\">\n <input\n type=\"checkbox\"\n [checked]=\"isAutoSaveEnabled\"\n (change)=\"isAutoSaveEnabled = !isAutoSaveEnabled\"\n />\n <span></span>\n <span translate>Auto save</span>\n </label>\n </div>\n\n <div\n class=\"btn-toolbar m-0 p-relative\"\n role=\"toolbar\"\n >\n <c8y-tabs-outlet\n class=\"elevation-none\"\n [outletName]=\"TAB_OUTLET_NAME\"\n [orientation]=\"'horizontal'\"\n [openFirstTab]=\"false\"\n ></c8y-tabs-outlet>\n <c8y-tab\n [icon]=\"'code'\"\n [label]=\"(config?.devMode ? TAB_WEBCOMPONENT_LABEL : TAB_HTML_LABEL) | translate\"\n [priority]=\"100\"\n [showAlways]=\"true\"\n [tabsOutlet]=\"TAB_OUTLET_NAME\"\n [isActive]=\"mode === 'code'\"\n (onSelect)=\"switchMode('code')\"\n ></c8y-tab>\n @if (!config?.devMode && !config?.legacy) {\n <c8y-tab\n [icon]=\"'c8y-css'\"\n [label]=\"TAB_CSS_LABEL | translate\"\n [priority]=\"0\"\n [tabsOutlet]=\"TAB_OUTLET_NAME\"\n [isActive]=\"mode === 'css'\"\n (onSelect)=\"switchMode('css')\"\n ></c8y-tab>\n }\n </div>\n\n @if (!isLoading) {\n @if (!(mode === 'css' && config?.devMode)) {\n <c8y-editor\n class=\"flex-grow d-block\"\n style=\"height: 450px\"\n [ngModel]=\"value\"\n (ngModelChange)=\"changeCode($event)\"\n [editorOptions]=\"{\n language,\n tabSize: 2,\n insertSpaces: true,\n minimap: { enabled: false }\n }\"\n (editorInit)=\"editorLoaded($event)\"\n ></c8y-editor>\n }\n } @else {\n <c8y-loading></c8y-loading>\n }\n </fieldset>\n</div>\n","import { AsyncPipe } from '@angular/common';\nimport { Component, DestroyRef, inject, OnDestroy, TemplateRef, ViewChild } from '@angular/core';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport { FormsModule } from '@angular/forms';\nimport { RouterModule } from '@angular/router';\nimport { AlertService, OptionsService } from '@c8y/ngx-components';\nimport {\n AssetPropertyMappingKeyRenamedNotification,\n AssetPropertyMappingsService,\n WidgetConfigService,\n renamePropertyKeyInCode\n} from '@c8y/ngx-components/context-dashboard';\nimport {\n GlobalContextState,\n LocalControlsComponent,\n PRESET_NAME,\n PresetName,\n REFRESH_OPTION\n} from '@c8y/ngx-components/global-context';\nimport { HtmlWidgetConfig } from './html-widget.model';\nimport { HtmlFrameComponent } from './html-frame/html-frame.component';\nimport { HtmlWidgetConfigService } from './html-widget-config.service';\nimport { WidgetCodeEditorComponent } from './widget-code-editor-section/widget-code-editor.component';\nimport { distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators';\nimport { gettext } from '@c8y/ngx-components/gettext';\n\n@Component({\n selector: 'c8y-html-widget-config',\n templateUrl: './html-widget-config.component.html',\n standalone: true,\n imports: [\n RouterModule,\n FormsModule,\n AsyncPipe,\n HtmlFrameComponent,\n WidgetCodeEditorComponent,\n LocalControlsComponent\n ]\n})\nexport class HtmlWidgetConfigComponent implements OnDestroy {\n @ViewChild('htmlPreview')\n set htmlPreviewTemplate(template: TemplateRef<any>) {\n if (template) {\n this.widgetConfigService.setPreview(template);\n } else {\n this.widgetConfigService.setPreview(null);\n }\n }\n alert = inject(AlertService);\n options = inject(OptionsService);\n htmlWidgetConfigService = inject(HtmlWidgetConfigService);\n widgetConfigService = inject(WidgetConfigService);\n assetPropertyMappings = inject(AssetPropertyMappingsService);\n controls: PresetName = PRESET_NAME.AUTO_REFRESH_ONLY;\n private readonly destroyRef = inject(DestroyRef);\n private realtimeControl$ = this.widgetConfigService.currentConfig$.pipe(\n map(\n config =>\n config?.refreshOption === REFRESH_OPTION.LIVE && config?.isAutoRefreshEnabled === true\n ),\n distinctUntilChanged()\n );\n propertyValues$ = this.widgetConfigService.currentConfig$.pipe(\n switchMap(config =>\n this.assetPropertyMappings.getValues$(config?.properties, this.realtimeControl$)\n )\n );\n globalContextState$ = this.widgetConfigService.currentConfig$.pipe(\n map(config => config as GlobalContextState)\n );\n\n constructor() {\n this.htmlWidgetConfigService.notify$\n .pipe(\n takeUntilDestroyed(this.destroyRef),\n filter(\n (n): n is AssetPropertyMappingKeyRenamedNotification =>\n n.type === 'asset-property-mapping-key-renamed'\n )\n )\n .subscribe(notification => this.handleRename(notification));\n }\n\n ngOnDestroy(): void {\n // sadly the service is component scoped\n // but still not recycled correctly. That is why we do\n // it here.\n this.htmlWidgetConfigService.destroy();\n }\n\n private handleRename(notification: AssetPropertyMappingKeyRenamedNotification): void {\n const { oldKey, newKey } = notification;\n const htmlWidgetConfig = this.widgetConfigService.currentConfig as HtmlWidgetConfig;\n if (!htmlWidgetConfig?.config) {\n return;\n }\n const newCode = renamePropertyKeyInCode(htmlWidgetConfig.config.code, oldKey, newKey);\n if (newCode === htmlWidgetConfig.config.code) {\n return;\n }\n const updated = { ...htmlWidgetConfig.config, code: newCode };\n this.htmlWidgetConfigService.save(updated);\n this.htmlWidgetConfigService.configChanged$.next(updated);\n this.alert.success(gettext('Renamed asset property and updated references in HTML code.'));\n }\n}\n","<c8y-widget-code-editor\n [config]=\"htmlWidgetConfigService.config$ | async\"\n [mode]=\"'code'\"\n></c8y-widget-code-editor>\n\n<ng-template #htmlPreview>\n @if ((widgetConfigService.currentConfig$ | async)?.displayMode !== 'dashboard') {\n <c8y-local-controls\n [controls]=\"controls\"\n [displayMode]=\"(widgetConfigService.currentConfig$ | async)?.displayMode\"\n [config]=\"globalContextState$ | async\"\n [disabled]=\"true\"\n ></c8y-local-controls>\n }\n <c8y-html-frame\n [config]=\"htmlWidgetConfigService.codeEditorChangeConfig$ | async\"\n [device]=\"(widgetConfigService.currentConfig$ | async).device\"\n [propertyValues$]=\"propertyValues$\"\n [useSalt]=\"true\"\n ></c8y-html-frame>\n</ng-template>\n","import { AsyncPi