From 684f2b7e9690f1bf3bb78a1e3c995349fbcd4942 Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Mon, 29 Jun 2026 17:51:15 +0300 Subject: [PATCH] Fix device-install wizard treating BOOLEAN/INTEGER form fields as strings The device-install wizard substituted ${...} placeholders via a plain string replace, so a quoted boolean macro like "cleanSession": "${mqttCleanSession}" parsed as the string "false" (truthy) instead of the JSON boolean false. Unchecking a boolean field (e.g. clean session) therefore had no effect and read as true in the resulting integration. Add a type-aware resolveTemplateJson() that, using the form-field type recorded in form.json, emits BOOLEAN/INTEGER placeholders occupying a whole quoted token as raw JSON values (unquoted) while leaving string fields and embedded placeholders quoted. Route all template-parse call sites through it. Fixes cleanSession, ssl, port and connectTimeoutSec across all existing device packages without any template changes. --- .../device-install-dialog.component.ts | 51 +++++++++++++++---- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.ts index 96c5f9ed72..437b544e92 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.ts @@ -114,6 +114,9 @@ export class TbDeviceInstallDialogComponent extends DialogComponent = {}; entityOutputs = new Map(); transportVars: Record = {}; + // form-field key → declared type. Used by resolveTemplateJson() to emit BOOLEAN/INTEGER + // placeholders as raw JSON values instead of quoted strings. + private fieldTypes = new Map(); gatewayDockerComposeContent: string | null = null; resolveMarkdownVariable: (key: string) => string | undefined = this._resolveMarkdownVariable.bind(this); @@ -499,6 +502,38 @@ export class TbDeviceInstallDialogComponent extends DialogComponent { + const type = this.fieldTypes.get(key); + if (type !== FormFieldType.BOOLEAN && type !== FormFieldType.INTEGER) { + return whole; + } + const res = this.resolveVariable(key); + if (res === undefined) { + return whole; + } + if (type === FormFieldType.BOOLEAN) { + return String(res).trim().toLowerCase() === 'true' ? 'true' : 'false'; + } + // INTEGER: emit a raw JSON number only when the value really is an integer; + // otherwise leave it quoted for the regular pass (preserves prior behaviour). + const trimmed = String(res).trim(); + const num = Number(trimmed); + return trimmed !== '' && Number.isInteger(num) ? String(num) : whole; + }); + return JSON.parse(this.resolveVariables(typed)); + } + private _resolveMarkdownVariable(key: string): string | undefined { const res = this.resolveVariable(key); if (res) { @@ -648,6 +683,7 @@ export class TbDeviceInstallDialogComponent extends DialogComponent { const raw = this.zipFiles.get(step.template); if (!raw) throw new Error(`Template file not found: ${step.template}`); - const resolved = this.resolveVariables(raw); - const template = JSON.parse(resolved); + const template = this.resolveTemplateJson(raw); switch (step.type) { case InstallStepType.DEVICE_PROFILE: { @@ -1016,8 +1050,7 @@ export class TbDeviceInstallDialogComponent extends DialogComponent { const raw = this.zipFiles.get(step.template); if (!raw) return null; - const resolved = this.resolveVariables(raw); - const template = JSON.parse(resolved); + const template = this.resolveTemplateJson(raw); switch (step.type) { case InstallStepType.DEVICE_PROFILE: @@ -1095,7 +1128,7 @@ export class TbDeviceInstallDialogComponent extends DialogComponent ({ key, value })); await firstValueFrom(this.attributeService.saveEntityAttributes( entityId, AttributeScope.SERVER_SCOPE, attrs, {ignoreErrors: true} @@ -1105,7 +1138,7 @@ export class TbDeviceInstallDialogComponent extends DialogComponent ({ key, value })); await firstValueFrom(this.attributeService.saveEntityAttributes( entityId, AttributeScope.SHARED_SCOPE, attrs, {ignoreErrors: true}