diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.html index a075cf5847..733ed0a906 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.html @@ -64,107 +64,12 @@ @case ('form') {
- @if (ws.formGroup) { -
- @for (field of ws.formFields; track field.key) { - @switch (field.type) { - @case ('BOOLEAN') { - - {{ field.label }} - - } - @case ('SELECT') { - - {{ field.label }} - - @for (opt of field.options; track opt.value) { - {{ opt.label }} - } - - @if (field.helpText) { - {{ field.helpText }} - } - @if (ws.formGroup.controls[field.key]?.hasError('required')) { - {{ field.label + ' is required' }} - } - - } - @case ('PASSWORD') { - - {{ field.label }} - - - @if (field.randomGenerator) { - - } - @if (field.helpText) { - {{ field.helpText }} - } - @if (ws.formGroup.controls[field.key]?.hasError('required')) { - {{ field.label + ' is required' }} - } - @if (ws.formGroup.controls[field.key]?.hasError('pattern')) { - {{ getPatternErrorMessage(field) }} - } - - } - @case ('INTEGER') { - - {{ field.label }} - - @if (field.helpText) { - {{ field.helpText }} - } - @if (ws.formGroup.controls[field.key]?.hasError('required')) { - {{ field.label + ' is required' }} - } - @if (ws.formGroup.controls[field.key]?.hasError('pattern')) { - {{ getPatternErrorMessage(field) }} - } - - } - @default { - - {{ field.label }} - - @if (field.randomGenerator) { - - } - @if (field.helpText) { - {{ field.helpText }} - } - @if (ws.formGroup.controls[field.key]?.hasError('required')) { - {{ field.label + ' is required' }} - } - @if (ws.formGroup.controls[field.key]?.hasError('pattern')) { - {{ getPatternErrorMessage(field) }} - } - - } - } - @if (field.type === 'BOOLEAN' && field.helpText) { -
{{ field.helpText }}
- } - @if (field.helpImage) { - - } - } -
- } + +
} 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 835c1c8cb4..53dc3ac523 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 @@ -108,7 +108,6 @@ export class TbDeviceInstallDialogComponent extends DialogComponent = {}; reviewMode = false; // Variable resolution state @@ -456,9 +455,9 @@ export class TbDeviceInstallDialogComponent extends DialogComponent's [resolveImagePath] input. + // Defined as a property so the template binding stays stable across change detection. + readonly resolveImagePathFn: (path: string) => string = (path: string) => this.resolveImagePath(path); // --- Variable Resolution --- @@ -537,7 +536,6 @@ export class TbDeviceInstallDialogComponent extends DialogComponent +@if (formGroup) { +
+ @for (field of fields; track field.key; let i = $index) { + + + @switch (field.type) { + @case (FormFieldType.BOOLEAN) { + {{ field.label }} + } + @case (FormFieldType.SELECT) { + + {{ field.label }} + + @for (opt of field.options; track opt.value) { + {{ opt.label }} + } + + @if (field.helpText) { + {{ field.helpText }} + } + @if (formGroup.controls[field.key]?.hasError('required')) { + {{ field.label + ' is required' }} + } + + } + @case (FormFieldType.PASSWORD) { + + {{ field.label }} + + + @if (field.randomGenerator) { + + } + @if (field.helpText) { + {{ field.helpText }} + } + @if (formGroup.controls[field.key]?.hasError('required')) { + {{ field.label + ' is required' }} + } + @if (formGroup.controls[field.key]?.hasError('pattern')) { + {{ getPatternErrorMessage(field) }} + } + + } + @case (FormFieldType.INTEGER) { + + {{ field.label }} + + @if (field.helpText) { + {{ field.helpText }} + } + @if (formGroup.controls[field.key]?.hasError('required')) { + {{ field.label + ' is required' }} + } + @if (formGroup.controls[field.key]?.hasError('pattern')) { + {{ getPatternErrorMessage(field) }} + } + + } + @default { + + {{ field.label }} + + @if (field.randomGenerator) { + + } + @if (field.helpText) { + {{ field.helpText }} + } + @if (formGroup.controls[field.key]?.hasError('required')) { + {{ field.label + ' is required' }} + } + @if (formGroup.controls[field.key]?.hasError('pattern')) { + {{ getPatternErrorMessage(field) }} + } + + } + } + + @if (field.type === FormFieldType.BOOLEAN && field.helpText) { +
{{ field.helpText }}
+ } + @if (field.helpImage) { + + } + } +
+} diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/install-form-renderer/install-form-renderer.component.scss b/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/install-form-renderer/install-form-renderer.component.scss new file mode 100644 index 0000000000..daaa824570 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/install-form-renderer/install-form-renderer.component.scss @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + display: block; +} + +.tb-install-form-field-help { + font-size: 12px; + color: rgba(0, 0, 0, 0.6); + margin-top: 4px; +} + +.tb-install-form-field-help-image { + max-width: 100%; + margin-top: 8px; +} diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/install-form-renderer/install-form-renderer.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/install-form-renderer/install-form-renderer.component.ts new file mode 100644 index 0000000000..ea129ce0ea --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/install-form-renderer/install-form-renderer.component.ts @@ -0,0 +1,82 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { UntypedFormGroup } from '@angular/forms'; +import { FormFieldDefinition, FormFieldType } from '@shared/models/iot-hub/device-package.models'; +import { generateSecret } from '@core/utils'; + +const DEFAULT_RANDOM_SIZE = 20; + +/** + * Reusable form renderer for the device install dialog's SHOW_FORM step. Consumes + * the FormFieldDefinition[] parsed from the package's form.json directly. + * + * The renderer is presentation-only. The caller owns the FormGroup and supplies + * the FormFieldDefinition[] array. Optional resolveImagePath callback maps + * relative help-image paths to data URIs (the dialog supplies it from the parsed + * package ZIP). + */ +@Component({ + selector: 'tb-install-form-renderer', + templateUrl: './install-form-renderer.component.html', + styleUrls: ['./install-form-renderer.component.scss'], + standalone: false +}) +export class InstallFormRendererComponent implements OnChanges { + + @Input() fields: FormFieldDefinition[] = []; + @Input() formGroup!: UntypedFormGroup; + @Input() resolveImagePath?: (path: string) => string; + /** When true, PASSWORD inputs render unmasked by default (used by review mode). */ + @Input() reviewMode = false; + + passwordVisible: Record = {}; + + readonly FormFieldType = FormFieldType; + + ngOnChanges(changes: SimpleChanges): void { + if (changes.fields || changes.reviewMode) { + this.passwordVisible = {}; + if (this.reviewMode) { + for (const f of this.fields) { + if (f.type === FormFieldType.PASSWORD) { + this.passwordVisible[f.key] = true; + } + } + } + } + } + + togglePasswordVisible(key: string): void { + this.passwordVisible[key] = !this.passwordVisible[key]; + } + + regenerate(field: FormFieldDefinition): void { + const control = this.formGroup.controls[field.key]; + if (!control) return; + control.patchValue(generateSecret(field.randomSize ?? DEFAULT_RANDOM_SIZE)); + control.markAsDirty(); + } + + getPatternErrorMessage(field: FormFieldDefinition): string { + return field.validators?.[0]?.message || 'Invalid format'; + } + + imagePath(path: string): string { + return this.resolveImagePath ? this.resolveImagePath(path) : path; + } +} diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.module.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.module.ts index bdd8dfa65f..cb37201432 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.module.ts @@ -26,6 +26,7 @@ import { TbIotHubUpdateDialogComponent } from './iot-hub-update-dialog.component import { TbIotHubDeleteDialogComponent } from './iot-hub-delete-dialog.component'; import { TbIotHubUnpublishedWarningDialogComponent } from './iot-hub-unpublished-warning-dialog.component'; import { TbDeviceInstallDialogComponent } from './device-install-dialog/device-install-dialog.component'; +import { InstallFormRendererComponent } from './device-install-dialog/install-form-renderer/install-form-renderer.component'; import { TbIotHubSearchComponent } from './iot-hub-search.component'; import { TbIotHubInstalledItemsTableComponent } from './iot-hub-installed-items-table.component'; import { TbIotHubInstalledItemsDialogComponent } from './iot-hub-installed-items-dialog.component'; @@ -51,7 +52,8 @@ import { IotHubItemLinkModule } from './iot-hub-item-link-card/iot-hub-item-link TbIotHubInstalledItemsDialogComponent, TbPeConnectivityMethodPromptComponent, TbIotHubMarkdownComponent, - SolutionInstallDialogComponent + SolutionInstallDialogComponent, + InstallFormRendererComponent ], imports: [ CommonModule,