From 4b38c2cd759c122d3fc67d61323b61ac94a866aa Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Thu, 9 Apr 2026 16:59:46 +0300 Subject: [PATCH] feat(iot-hub): use tabs instead of stepper for read-only review dialog Replace mat-stepper with mat-tab-group in review mode for a cleaner read-only experience. All tabs are freely clickable without linear progression. - Extract step content into shared ng-template (used by both stepper and tabs to avoid duplication) - Pre-activate all step content on init in review mode (tabs render lazily) - Add onTabChanged handler for tab selection - Footer shows Close + Open Dashboard buttons - Install mode stepper unchanged --- .../device-install-dialog.component.html | 348 ++++++++++-------- .../device-install-dialog.component.scss | 11 + .../device-install-dialog.component.ts | 27 +- 3 files changed, 224 insertions(+), 162 deletions(-) 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 9f59507941..6e34f99e35 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 @@ -15,6 +15,162 @@ limitations under the License. --> + + + +
+ @switch (ws.type) { + + @case ('instruction') { +
+ + +
+ } + + @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 (ws.formGroup.controls[field.key]?.hasError('required')) { + {{ field.label + ' is required' }} + } + + } + @case ('PASSWORD') { + + {{ field.label }} + + + @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 (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 (ws.formGroup.controls[field.key]?.hasError('required')) { + {{ field.label + ' is required' }} + } + @if (ws.formGroup.controls[field.key]?.hasError('pattern')) { + {{ getPatternErrorMessage(field) }} + } + + } + } + @if (field.helpText) { +
{{ field.helpText }}
+ } + @if (field.helpImage) { + + } + } +
+ } +
+ } + + @case ('progress') { +
+ @for (ep of ws.entitySteps; track ep.step.name) { +
+
+ @switch (ep.status) { + @case ('pending') { + radio_button_unchecked + } + @case ('running') { + + } + @case ('success') { + check_circle + } + @case ('error') { + error + } + @case ('conflict') { + warning + } + } +
+
+ {{ ('iot-hub.device-install-step-type-' + ep.step.type | translate) + ' — ' + (ep.resolvedName || ep.step.name) }} + @if (ep.status === 'error' && ep.errorMessage) { + {{ ep.errorMessage }} + } + @if (ep.status === 'conflict') { + {{ 'iot-hub.device-install-conflict-exists' | translate:{ type: ('iot-hub.device-install-step-type-' + ep.step.type | translate) } }} + } +
+ @if (ep.status === 'success' && ep.resolution) { + {{ 'iot-hub.device-install-resolution-' + ep.resolution | translate }} + } + @if (ep.status === 'conflict') { +
+ @if (ep.conflictType === 'use-or-overwrite') { + + + } @else { + + + } +
+ } +
+ } +
+ } + } +
+
+ + @if (loading) {
@@ -56,182 +212,54 @@ {{ 'action.next' | translate }} + + } @else if (reviewMode) { + +
+ + @for (ws of wizardSteps; track ws.label) { + + +
+ +
+
+
+ } +
+
+ + + @if (primaryEntityAction; as action) { + + } + + } @else { - +
+ [linear]="true" + labelPosition="bottom"> done @for (ws of wizardSteps; track ws.label; let i = $index; let last = $last) { - + [stepControl]="ws.type === 'form' ? ws.formGroup : null"> {{ ws.label }} - -
- @switch (ws.type) { - - @case ('instruction') { -
- - -
- } - - @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 (ws.formGroup.controls[field.key]?.hasError('required')) { - {{ field.label + ' is required' }} - } - - } - @case ('PASSWORD') { - - {{ field.label }} - - - @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 (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 (ws.formGroup.controls[field.key]?.hasError('required')) { - {{ field.label + ' is required' }} - } - @if (ws.formGroup.controls[field.key]?.hasError('pattern')) { - {{ getPatternErrorMessage(field) }} - } - - } - } - @if (field.helpText) { -
{{ field.helpText }}
- } - @if (field.helpImage) { - - } - } -
- } -
- } - - @case ('progress') { -
- @for (ep of ws.entitySteps; track ep.step.name) { -
-
- @switch (ep.status) { - @case ('pending') { - radio_button_unchecked - } - @case ('running') { - - } - @case ('success') { - check_circle - } - @case ('error') { - error - } - @case ('conflict') { - warning - } - } -
-
- {{ ('iot-hub.device-install-step-type-' + ep.step.type | translate) + ' — ' + (ep.resolvedName || ep.step.name) }} - @if (ep.status === 'error' && ep.errorMessage) { - {{ ep.errorMessage }} - } - @if (ep.status === 'conflict') { - {{ 'iot-hub.device-install-conflict-exists' | translate:{ type: ('iot-hub.device-install-step-type-' + ep.step.type | translate) } }} - } -
- @if (ep.status === 'success' && ep.resolution) { - {{ 'iot-hub.device-install-resolution-' + ep.resolution | translate }} - } - @if (ep.status === 'conflict') { -
- @if (ep.conflictType === 'use-or-overwrite') { - - - } @else { - - - } -
- } -
- } -
- } - } -
+
}
- @if (reviewMode) { - - } @else if (currentWizardStep; as step) { + @if (currentWizardStep; as step) { @switch (step.type) { @case ('instruction') { @if (isLastWizardStep) { diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.scss b/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.scss index c484735696..8083e6a44a 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.scss @@ -97,6 +97,17 @@ } } +// Tabs container (review mode) +.tb-device-install-tabs-container { + flex: 1; + overflow-y: auto; + min-height: 200px; + + .tb-tab-content { + padding: 16px 24px; + } +} + // Stepper container .tb-device-install-stepper-container { flex: 1; 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 403777bfe0..9db5975469 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 @@ -190,6 +190,17 @@ export class TbDeviceInstallDialogComponent extends DialogComponent this.onStepActivated(), 0); + if (this.reviewMode) { + // Pre-activate all steps for tab mode (tabs render lazily on first select) + for (const ws of this.wizardSteps) { + if (ws.type === 'instruction') { + const raw = this.zipFiles.get(ws.rawSteps[0].file) || ''; + ws.markdown = this.resolveImages(this.resolveVariables(raw)); + } else if (ws.type === 'progress') { + this.showCompletedEntitySteps(ws); + } + } + } else { + // Activate the first step after the stepper renders + setTimeout(() => this.onStepActivated(), 0); + } } private buildWizardSteps(): void {