Browse Source
The form rendering for the SHOW_FORM step was inline in the dialog template. Extract it into a presentation-only InstallFormRendererComponent next to the dialog. The dialog now binds [fields], [formGroup], [resolveImagePath], and [reviewMode] inputs. [reviewMode] consolidates the previous dialog-side passwordVisible[key]=true assignment into the renderer's own state — the dialog no longer needs to track per-field password visibility. Layout mirrors thingsboard-pe so changes propagate cleanly between branches.pull/15675/head
committed by
GitHub
6 changed files with 242 additions and 119 deletions
@ -0,0 +1,119 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
--> |
||||
|
@if (formGroup) { |
||||
|
<form [formGroup]="formGroup" class="tb-install-form flex flex-col gap-4"> |
||||
|
@for (field of fields; track field.key; let i = $index) { |
||||
|
|
||||
|
<!-- typed widget --> |
||||
|
@switch (field.type) { |
||||
|
@case (FormFieldType.BOOLEAN) { |
||||
|
<mat-checkbox [formControlName]="field.key">{{ field.label }}</mat-checkbox> |
||||
|
} |
||||
|
@case (FormFieldType.SELECT) { |
||||
|
<mat-form-field appearance="outline"> |
||||
|
<mat-label>{{ field.label }}</mat-label> |
||||
|
<mat-select [formControlName]="field.key"> |
||||
|
@for (opt of field.options; track opt.value) { |
||||
|
<mat-option [value]="opt.value">{{ opt.label }}</mat-option> |
||||
|
} |
||||
|
</mat-select> |
||||
|
@if (field.helpText) { |
||||
|
<mat-hint>{{ field.helpText }}</mat-hint> |
||||
|
} |
||||
|
@if (formGroup.controls[field.key]?.hasError('required')) { |
||||
|
<mat-error>{{ field.label + ' is required' }}</mat-error> |
||||
|
} |
||||
|
</mat-form-field> |
||||
|
} |
||||
|
@case (FormFieldType.PASSWORD) { |
||||
|
<mat-form-field appearance="outline"> |
||||
|
<mat-label>{{ field.label }}</mat-label> |
||||
|
<input matInput [type]="passwordVisible[field.key] ? 'text' : 'password'" |
||||
|
[formControlName]="field.key"> |
||||
|
<button mat-icon-button matSuffix type="button" |
||||
|
(click)="togglePasswordVisible(field.key)"> |
||||
|
<mat-icon>{{ passwordVisible[field.key] ? 'visibility_off' : 'visibility' }}</mat-icon> |
||||
|
</button> |
||||
|
@if (field.randomGenerator) { |
||||
|
<button mat-icon-button matSuffix type="button" |
||||
|
[matTooltip]="'iot-hub.generate-random-value' | translate" |
||||
|
matTooltipPosition="above" |
||||
|
(click)="regenerate(field)"> |
||||
|
<mat-icon>autorenew</mat-icon> |
||||
|
</button> |
||||
|
} |
||||
|
@if (field.helpText) { |
||||
|
<mat-hint>{{ field.helpText }}</mat-hint> |
||||
|
} |
||||
|
@if (formGroup.controls[field.key]?.hasError('required')) { |
||||
|
<mat-error>{{ field.label + ' is required' }}</mat-error> |
||||
|
} |
||||
|
@if (formGroup.controls[field.key]?.hasError('pattern')) { |
||||
|
<mat-error>{{ getPatternErrorMessage(field) }}</mat-error> |
||||
|
} |
||||
|
</mat-form-field> |
||||
|
} |
||||
|
@case (FormFieldType.INTEGER) { |
||||
|
<mat-form-field appearance="outline"> |
||||
|
<mat-label>{{ field.label }}</mat-label> |
||||
|
<input matInput type="number" [formControlName]="field.key"> |
||||
|
@if (field.helpText) { |
||||
|
<mat-hint>{{ field.helpText }}</mat-hint> |
||||
|
} |
||||
|
@if (formGroup.controls[field.key]?.hasError('required')) { |
||||
|
<mat-error>{{ field.label + ' is required' }}</mat-error> |
||||
|
} |
||||
|
@if (formGroup.controls[field.key]?.hasError('pattern')) { |
||||
|
<mat-error>{{ getPatternErrorMessage(field) }}</mat-error> |
||||
|
} |
||||
|
</mat-form-field> |
||||
|
} |
||||
|
@default { |
||||
|
<mat-form-field appearance="outline"> |
||||
|
<mat-label>{{ field.label }}</mat-label> |
||||
|
<input matInput [formControlName]="field.key"> |
||||
|
@if (field.randomGenerator) { |
||||
|
<button mat-icon-button matSuffix type="button" |
||||
|
[matTooltip]="'iot-hub.generate-random-value' | translate" |
||||
|
matTooltipPosition="above" |
||||
|
(click)="regenerate(field)"> |
||||
|
<mat-icon>autorenew</mat-icon> |
||||
|
</button> |
||||
|
} |
||||
|
@if (field.helpText) { |
||||
|
<mat-hint>{{ field.helpText }}</mat-hint> |
||||
|
} |
||||
|
@if (formGroup.controls[field.key]?.hasError('required')) { |
||||
|
<mat-error>{{ field.label + ' is required' }}</mat-error> |
||||
|
} |
||||
|
@if (formGroup.controls[field.key]?.hasError('pattern')) { |
||||
|
<mat-error>{{ getPatternErrorMessage(field) }}</mat-error> |
||||
|
} |
||||
|
</mat-form-field> |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@if (field.type === FormFieldType.BOOLEAN && field.helpText) { |
||||
|
<div class="tb-install-form-field-help">{{ field.helpText }}</div> |
||||
|
} |
||||
|
@if (field.helpImage) { |
||||
|
<img class="tb-install-form-field-help-image" [src]="imagePath(field.helpImage)" [alt]="field.label"> |
||||
|
} |
||||
|
} |
||||
|
</form> |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
@ -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<string, boolean> = {}; |
||||
|
|
||||
|
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; |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue