UX Overhaul Part 1 (#3047)

Co-authored-by: Joseph Milazzo <joseph.v.milazzo@gmail.com>
This commit is contained in:
Robbie Davis 2024-08-09 13:55:31 -04:00 committed by GitHub
parent 5934d516f3
commit ff79710ac6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
324 changed files with 11589 additions and 4598 deletions

View file

@ -0,0 +1,179 @@
<ng-container *transloco="let t; read: 'import-cbl-modal'">
<div class="row g-0" style="min-width: 135px;">
<app-step-tracker [steps]="steps" [currentStep]="currentStepIndex"></app-step-tracker>
</div>
<!-- This is going to need to have a fixed height with a scrollbar-->
<div>
@switch (currentStepIndex) {
@case (Step.Import) {
<div class="row g-0">
<p>{{t('import-description')}}</p>
<form [formGroup]="uploadForm" enctype="multipart/form-data">
<file-upload formControlName="files"></file-upload>
</form>
</div>
}
@case (Step.Validate) {
<p>{{t('validate-description')}}</p>
<div class="row g-0">
<div ngbAccordion #accordion="ngbAccordion">
@for(fileToProcess of filesToProcess; track fileToProcess.fileName) {
@if (fileToProcess.validateSummary; as summary) {
<div ngbAccordionItem>
<h5 ngbAccordionHeader>
<button ngbAccordionButton>
<ng-container [ngTemplateOutlet]="heading" [ngTemplateOutletContext]="{ summary: summary, filename: fileToProcess.fileName }"></ng-container>
</button>
</h5>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
<ng-container [ngTemplateOutlet]="validationList" [ngTemplateOutletContext]="{ summary: summary }"></ng-container>
</div>
</div>
</div>
}
}
</div>
</div>
}
@case (Step.DryRun) {
<div class="row g-0">
<p>{{t('dry-run-description')}}</p>
<div ngbAccordion #a="ngbAccordion">
@for(fileToProcess of filesToProcess; track fileToProcess.fileName) {
@if (fileToProcess.dryRunSummary; as summary) {
<div ngbAccordionItem>
<h5 ngbAccordionHeader>
<button ngbAccordionButton>
<ng-container [ngTemplateOutlet]="heading" [ngTemplateOutletContext]="{ summary: summary, filename: fileToProcess.fileName }"></ng-container>
</button>
</h5>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
<ng-container [ngTemplateOutlet]="resultsList" [ngTemplateOutletContext]="{ summary: summary }"></ng-container>
</div>
</div>
</div>
}
}
</div>
</div>
}
@case (Step.Finalize) {
<div class="row g-0">
<div ngbAccordion #a="ngbAccordion">
@for(fileToProcess of filesToProcess; track fileToProcess.fileName) {
@if (fileToProcess.finalizeSummary; as summary) {
<div ngbAccordionItem>
<h5 ngbAccordionHeader>
<button ngbAccordionButton>
<ng-container [ngTemplateOutlet]="heading" [ngTemplateOutletContext]="{ summary: summary, filename: fileToProcess.fileName }"></ng-container>
</button>
</h5>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
<ng-container [ngTemplateOutlet]="resultsList" [ngTemplateOutletContext]="{ summary: summary }"></ng-container>
</div>
</div>
</div>
}
}
</div>
</div>
}
}
</div>
<ng-template #validationList let-summary="summary">
@if (summary.results.length > 0) {
<div class="justify-content-center col">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<i class="fa-solid fa-triangle-exclamation" style="font-size: 24px" aria-hidden="true"></i>
</div>
<div class="flex-grow-1 ms-3">
{{t('validate-warning')}}
</div>
</div>
</div>
<ol class="list-group list-group-numbered list-group-flush" >
@for(result of summary.results; track result) {
<li class="list-group-item no-hover"
[innerHTML]="result | cblConflictReason | safeHtml">
</li>
}
</ol>
}
@else {
<div class="justify-content-center col">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<i class="fa-solid fa-circle-check" style="font-size: 24px" aria-hidden="true"></i>
</div>
<div class="flex-grow-1 ms-3">
{{t('validate-no-issue-description')}}
</div>
</div>
</div>
}
</ng-template>
<ng-template #resultsList let-summary="summary">
<ul class="list-group list-group-flush">
@for(result of summary.results; track result.order) {
<li class="list-group-item no-hover"
innerHTML="{{result.order + 1}}. {{result | cblConflictReason | safeHtml}}"></li>
}
</ul>
</ng-template>
<ng-template #heading let-filename="filename" let-summary="summary">
@switch (summary.success) {
@case (CblImportResult.Success) {
<span class="badge heading-badge bg-primary me-1">{{summary.success | cblImportResult}}</span>
}
@case (CblImportResult.Fail) {
<span class="badge heading-badge bg-danger me-1">{{summary.success | cblImportResult}}</span>
}
@case (CblImportResult.Partial) {
<span class="badge heading-badge bg-warning me-1">{{summary.success | cblImportResult}}</span>
}
}
<span>{{filename}}
@if(summary.cblName) {
<span>: ({{summary.cblName}})</span>
}
</span>
</ng-template>
<div class="modal-footer mt-3">
<form [formGroup]="cblSettingsForm" class="row align-items-center">
<div class="col-auto">
<div class="form-check form-switch">
<input type="checkbox" id="settings-comicvine-mode" role="switch" formControlName="comicVineMatching" class="form-check-input"
aria-labelledby="auto-close-label">
<label class="form-check-label" for="settings-comicvine-mode">{{t('comicvine-parsing-label')}}</label>
</div>
</div>
</form>
<!-- Spacer -->
<div class="col" aria-hidden="true"></div>
<div class="col-auto ms-1">
<a class="btn btn-icon" [href]="WikiLink.ReadingListCBL" target="_blank" rel="noopener noreferrer">Help</a>
</div>
<div class="col-auto ms-1">
<button type="button" class="btn btn-primary" (click)="prevStep()" [disabled]="!canMoveToPrevStep()">{{t('prev')}}</button>
</div>
<div class="col-auto ms-1">
<button type="button" class="btn btn-primary" (click)="nextStep()" [disabled]="!canMoveToNextStep()">{{t(NextButtonLabel)}}</button>
</div>
</div>
</ng-container>

View file

@ -43,3 +43,4 @@ file-upload {
::ng-deep .reading-list-fail--item {
color: var(--error-color);
}

View file

@ -1,23 +1,29 @@
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, inject, ViewChild} from '@angular/core';
import {FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms';
import {FileUploadModule, FileUploadValidators} from '@iplab/ngx-file-upload';
import {
NgbAccordionModule, NgbAccordionToggle,
NgbActiveModal
} from '@ng-bootstrap/ng-bootstrap';
import { ToastrService } from 'ngx-toastr';
import { forkJoin } from 'rxjs';
import { UtilityService } from 'src/app/shared/_services/utility.service';
import { CblImportResult } from 'src/app/_models/reading-list/cbl/cbl-import-result.enum';
import { CblImportSummary } from 'src/app/_models/reading-list/cbl/cbl-import-summary';
import { ReadingListService } from 'src/app/_services/reading-list.service';
import {StepTrackerComponent, TimelineStep} from '../../_components/step-tracker/step-tracker.component';
import {CommonModule} from "@angular/common";
import {SafeHtmlPipe} from "../../../_pipes/safe-html.pipe";
import {CblConflictReasonPipe} from "../../../_pipes/cbl-conflict-reason.pipe";
import {CblImportResultPipe} from "../../../_pipes/cbl-import-result.pipe";
import {FileUploadComponent, FileUploadValidators} from "@iplab/ngx-file-upload";
import {FormControl, FormGroup, FormsModule, ReactiveFormsModule} from "@angular/forms";
import {NgForOf, NgIf, NgTemplateOutlet} from "@angular/common";
import {
NgbAccordionBody,
NgbAccordionButton,
NgbAccordionCollapse,
NgbAccordionDirective,
NgbAccordionHeader,
NgbAccordionItem,
NgbActiveModal
} from "@ng-bootstrap/ng-bootstrap";
import {SafeHtmlPipe} from "../../../_pipes/safe-html.pipe";
import {StepTrackerComponent, TimelineStep} from "../step-tracker/step-tracker.component";
import {translate, TranslocoDirective} from "@ngneat/transloco";
import {WikiLink} from "../../../_models/wiki";
import {ReadingListService} from "../../../_services/reading-list.service";
import {UtilityService} from "../../../shared/_services/utility.service";
import {ToastrService} from "ngx-toastr";
import {forkJoin} from "rxjs";
import {CblImportSummary} from "../../../_models/reading-list/cbl/cbl-import-summary";
import { WikiLink } from 'src/app/_models/wiki';
import { CblImportResult } from 'src/app/_models/reading-list/cbl/cbl-import-result.enum';
interface FileStep {
fileName: string;
@ -34,18 +40,35 @@ enum Step {
}
@Component({
selector: 'app-import-cbl-modal',
selector: 'app-import-cbl',
standalone: true,
imports: [CommonModule,
FileUploadModule,
NgbAccordionModule,
imports: [
CblConflictReasonPipe,
CblImportResultPipe,
FileUploadComponent,
FormsModule,
NgbAccordionBody,
NgbAccordionButton,
NgbAccordionCollapse,
NgbAccordionDirective,
NgbAccordionHeader,
NgbAccordionItem,
ReactiveFormsModule,
SafeHtmlPipe,
CblConflictReasonPipe, ReactiveFormsModule, StepTrackerComponent, CblImportResultPipe, NgbAccordionToggle, TranslocoDirective],
templateUrl: './import-cbl-modal.component.html',
styleUrls: ['./import-cbl-modal.component.scss'],
StepTrackerComponent,
TranslocoDirective,
NgTemplateOutlet
],
templateUrl: './import-cbl.component.html',
styleUrl: './import-cbl.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ImportCblModalComponent {
export class ImportCblComponent {
private readonly readingListService = inject(ReadingListService);
private readonly toastr = inject(ToastrService);
private readonly cdRef = inject(ChangeDetectorRef);
protected readonly utilityService = inject(UtilityService);
protected readonly CblImportResult = CblImportResult;
protected readonly Step = Step;
@ -90,13 +113,7 @@ export class ImportCblModalComponent {
}
}
constructor(private ngModal: NgbActiveModal, private readingListService: ReadingListService,
public utilityService: UtilityService, private readonly cdRef: ChangeDetectorRef,
private toastr: ToastrService) {}
close() {
this.ngModal.close();
}
nextStep() {
if (this.currentStepIndex === Step.Import && !this.isFileSelected()) return;
@ -113,11 +130,12 @@ export class ImportCblModalComponent {
const pages = [];
for (let i = 0; i < files.length; i++) {
const formData = new FormData();
formData.append('cbl', files[i]);
formData.append('dryRun', 'true');
formData.append('comicVineMatching', this.cblSettingsForm.get('comicVineMatching')?.value + '');
pages.push(this.readingListService.validateCbl(formData));
formData.append('cbl', files[i]);
formData.append('dryRun', 'true');
formData.append('comicVineMatching', this.cblSettingsForm.get('comicVineMatching')?.value + '');
pages.push(this.readingListService.validateCbl(formData));
}
forkJoin(pages).subscribe(results => {
this.filesToProcess = [];
results.forEach(cblImport => {
@ -210,7 +228,7 @@ export class ImportCblModalComponent {
pages.push(this.readingListService.importCbl(formData));
}
forkJoin(pages).subscribe(results => {
results.forEach(cblImport => {
results.forEach(cblImport => {
const index = this.filesToProcess.findIndex(p => p.fileName === cblImport.fileName);
this.filesToProcess[index].dryRunSummary = cblImport;
});
@ -234,6 +252,7 @@ export class ImportCblModalComponent {
formData.append('comicVineMatching', this.cblSettingsForm.get('comicVineMatching')?.value + '');
pages.push(this.readingListService.importCbl(formData));
}
forkJoin(pages).subscribe(results => {
results.forEach(cblImport => {
const index = this.filesToProcess.findIndex(p => p.fileName === cblImport.fileName);

View file

@ -1,17 +1,17 @@
<ng-container *transloco="let t; read: 'reading-list-detail'">
<app-side-nav-companion-bar [hasExtras]="readingList !== undefined" [extraDrawer]="extrasDrawer">
<h2 title>
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [attr.aria-labelledby]="readingList?.title" *ngIf="actions.length > 0"></app-card-actionables>
<h4 title>
{{readingList?.title}}
@if (readingList?.promoted) {
<span class="ms-1">(<i class="fa fa-angle-double-up" aria-hidden="true"></i>)</span>
}
</h2>
<h6 subtitle class="subtitle-with-actionables">{{t('item-count', {num: items.length | number})}}</h6>
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [attr.aria-labelledby]="readingList?.title" *ngIf="actions.length > 0"></app-card-actionables>
</h4>
<h5 subtitle class="subtitle-with-actionables">{{t('item-count', {num: items.length | number})}}</h5>
<ng-template #extrasDrawer let-offcanvas>
@if (readingList) {
<div style="margin-top: 56px">
<div>
<div class="offcanvas-header">
<h4 class="offcanvas-title" id="offcanvas-basic-title">{{t('page-settings-title')}}</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="offcanvas.dismiss()"></button>

View file

@ -1,11 +1,11 @@
<ng-container *transloco="let t; read: 'reading-lists'">
<app-side-nav-companion-bar>
<h2 title>
<app-card-actionables [actions]="globalActions" (actionHandler)="performGlobalAction($event)"></app-card-actionables>
<h4 title>
<span>{{t('title')}}</span>
</h2>
<app-card-actionables [actions]="globalActions" (actionHandler)="performGlobalAction($event)"></app-card-actionables>
</h4>
@if (pagination) {
<h6 subtitle class="subtitle-with-actionables">{{t('item-count', {num: pagination.totalItems | number})}}</h6>
<h5 subtitle class="subtitle-with-actionables">{{t('item-count', {num: pagination.totalItems | number})}}</h5>
}
</app-side-nav-companion-bar>

View file

@ -12,21 +12,17 @@ import { ActionService } from 'src/app/_services/action.service';
import { ImageService } from 'src/app/_services/image.service';
import { JumpbarService } from 'src/app/_services/jumpbar.service';
import { ReadingListService } from 'src/app/_services/reading-list.service';
import { ImportCblModalComponent } from '../../_modals/import-cbl-modal/import-cbl-modal.component';
import { CardItemComponent } from '../../../cards/card-item/card-item.component';
import { CardDetailLayoutComponent } from '../../../cards/card-detail-layout/card-detail-layout.component';
import { NgIf, DecimalPipe } from '@angular/common';
import { SideNavCompanionBarComponent } from '../../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component';
import {translate, TranslocoDirective, TranslocoService} from "@ngneat/transloco";
import {translate, TranslocoDirective} from "@ngneat/transloco";
import {CardActionablesComponent} from "../../../_single-module/card-actionables/card-actionables.component";
import {Title} from "@angular/platform-browser";
import {WikiLink} from "../../../_models/wiki";
import {BulkSelectionService} from "../../../cards/bulk-selection.service";
import {BulkOperationsComponent} from "../../../cards/bulk-operations/bulk-operations.component";
import {KEY_CODES} from "../../../shared/_services/utility.service";
import {UserCollection} from "../../../_models/collection-tag";
import {User} from "../../../_models/user";
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({
selector: 'app-reading-lists',
@ -50,8 +46,8 @@ export class ReadingListsComponent implements OnInit {
hasPromote: boolean = false;
jumpbarKeys: Array<JumpKey> = [];
actions: {[key: number]: Array<ActionItem<ReadingList>>} = {};
globalActions: Array<ActionItem<any>> = [{action: Action.Import, title: 'import-cbl', children: [], requiresAdmin: true, callback: this.importCbl.bind(this)}];
trackByIdentity = (index: number, item: ReadingList) => `${item.id}_${item.title}`;
globalActions: Array<ActionItem<any>> = [];
trackByIdentity = (index: number, item: ReadingList) => `${item.id}_${item.title}_${item.promoted}`;
@HostListener('document:keydown.shift', ['$event'])
handleKeypress(event: KeyboardEvent) {
@ -88,6 +84,7 @@ export class ReadingListsComponent implements OnInit {
getActions(readingList: ReadingList) {
const d = this.actionFactoryService.getReadingListActions(this.handleReadingListActionCallback.bind(this))
.filter(action => this.readingListService.actionListFilter(action, readingList, this.isAdmin || this.hasPromote));
return this.actionFactoryService.getReadingListActions(this.handleReadingListActionCallback.bind(this))
.filter(action => this.readingListService.actionListFilter(action, readingList, this.isAdmin || this.hasPromote));
}
@ -104,12 +101,6 @@ export class ReadingListsComponent implements OnInit {
}
}
importCbl() {
const ref = this.ngbModal.open(ImportCblModalComponent, {size: 'xl', fullscreen: 'md'});
ref.closed.subscribe(result => this.loadPage());
ref.dismissed.subscribe(_ => this.loadPage());
}
handleReadingListActionCallback(action: ActionItem<ReadingList>, readingList: ReadingList) {
switch(action.action) {
case Action.Delete:
@ -125,6 +116,22 @@ export class ReadingListsComponent implements OnInit {
this.cdRef.markForCheck();
});
break;
case Action.Promote:
this.actionService.promoteMultipleReadingLists([readingList], true, (res) => {
// Reload information around list
readingList.promoted = true;
this.loadPage();
this.cdRef.markForCheck();
});
break;
case Action.UnPromote:
this.actionService.promoteMultipleReadingLists([readingList], false, (res) => {
// Reload information around list
readingList.promoted = false;
this.loadPage();
this.cdRef.markForCheck();
});
break;
}
}

View file

@ -1,174 +0,0 @@
<ng-container *transloco="let t; read: 'import-cbl-modal'">
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{t('title')}}</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="close()"></button>
</div>
<div class="modal-body scrollable-modal">
<div class="row g-0" style="min-width: 135px;">
<app-step-tracker [steps]="steps" [currentStep]="currentStepIndex"></app-step-tracker>
</div>
<!-- This is going to need to have a fixed height with a scrollbar-->
<div>
<div class="row g-0" *ngIf="currentStepIndex === Step.Import">
<p>{{t('import-description')}}</p>
<form [formGroup]="uploadForm" enctype="multipart/form-data">
<file-upload formControlName="files"></file-upload>
</form>
</div>
<ng-container *ngIf="currentStepIndex === Step.Validate">
<p>{{t('validate-description')}}</p>
<div class="row g-0">
<div ngbAccordion #accordion="ngbAccordion">
@for(fileToProcess of filesToProcess; track fileToProcess.fileName) {
<div ngbAccordionItem *ngIf="fileToProcess.validateSummary as summary">
<h5 ngbAccordionHeader>
<button ngbAccordionButton>
<ng-container [ngTemplateOutlet]="heading" [ngTemplateOutletContext]="{ summary: summary, filename: fileToProcess.fileName }"></ng-container>
</button>
</h5>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
<ng-container [ngTemplateOutlet]="validationList" [ngTemplateOutletContext]="{ summary: summary }"></ng-container>
</div>
</div>
</div>
}
</div>
</div>
</ng-container>
<ng-container *ngIf="currentStepIndex === Step.DryRun">
<div class="row g-0">
<p>{{t('dry-run-description')}}</p>
<div ngbAccordion #a="ngbAccordion">
@for(fileToProcess of filesToProcess; track fileToProcess.fileName) {
<div ngbAccordionItem *ngIf="fileToProcess.dryRunSummary as summary">
<h5 ngbAccordionHeader>
<button ngbAccordionButton>
<ng-container [ngTemplateOutlet]="heading" [ngTemplateOutletContext]="{ summary: summary, filename: fileToProcess.fileName }"></ng-container>
</button>
</h5>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
<ng-container [ngTemplateOutlet]="resultsList" [ngTemplateOutletContext]="{ summary: summary }"></ng-container>
</div>
</div>
</div>
}
</div>
</div>
</ng-container>
<ng-container *ngIf="currentStepIndex === Step.Finalize">
<div class="row g-0">
<div ngbAccordion #a="ngbAccordion">
@for(fileToProcess of filesToProcess; track fileToProcess.fileName) {
<div ngbAccordionItem *ngIf="fileToProcess.finalizeSummary as summary">
<h5 ngbAccordionHeader>
<button ngbAccordionButton>
<ng-container [ngTemplateOutlet]="heading" [ngTemplateOutletContext]="{ summary: summary, filename: fileToProcess.fileName }"></ng-container>
</button>
</h5>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
<ng-container [ngTemplateOutlet]="resultsList" [ngTemplateOutletContext]="{ summary: summary }"></ng-container>
</div>
</div>
</div>
}
</div>
</div>
</ng-container>
</div>
<ng-template #validationList let-summary="summary">
@if (summary.results.length > 0) {
<div class="justify-content-center col">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<i class="fa-solid fa-triangle-exclamation" style="font-size: 24px" aria-hidden="true"></i>
</div>
<div class="flex-grow-1 ms-3">
{{t('validate-warning')}}
</div>
</div>
</div>
<ol class="list-group list-group-numbered list-group-flush" >
<li class="list-group-item no-hover" *ngFor="let result of summary.results"
[innerHTML]="result | cblConflictReason | safeHtml">
</li>
</ol>
}
@else {
<div class="justify-content-center col">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<i class="fa-solid fa-circle-check" style="font-size: 24px" aria-hidden="true"></i>
</div>
<div class="flex-grow-1 ms-3">
{{t('validate-no-issue-description')}}
</div>
</div>
</div>
}
</ng-template>
<ng-template #resultsList let-summary="summary">
<ul class="list-group list-group-flush">
@for(result of summary.results; track result.order) {
<li class="list-group-item no-hover"
innerHTML="{{result.order + 1}}. {{result | cblConflictReason | safeHtml}}"></li>
}
</ul>
</ng-template>
<ng-template #heading let-filename="filename" let-summary="summary">
@switch (summary.success) {
@case (CblImportResult.Success) {
<span class="badge heading-badge bg-primary me-1">{{summary.success | cblImportResult}}</span>
}
@case (CblImportResult.Fail) {
<span class="badge heading-badge bg-danger me-1">{{summary.success | cblImportResult}}</span>
}
@case (CblImportResult.Partial) {
<span class="badge heading-badge bg-warning me-1">{{summary.success | cblImportResult}}</span>
}
}
<span>{{filename}}<span *ngIf="summary.cblName">: ({{summary.cblName}})</span></span>
</ng-template>
</div>
<div class="modal-footer">
<form [formGroup]="cblSettingsForm" class="row align-items-center">
<div class="col-auto">
<div class="form-check form-switch">
<input type="checkbox" id="settings-comicvine-mode" role="switch" formControlName="comicVineMatching" class="form-check-input"
aria-labelledby="auto-close-label">
<label class="form-check-label" for="settings-comicvine-mode">{{t('comicvine-parsing-label')}}</label>
</div>
</div>
</form>
<!-- Spacer -->
<div class="col" aria-hidden="true"></div>
<div class="col-auto">
<a class="btn btn-icon" [href]="WikiLink.ReadingListCBL" target="_blank" rel="noopener noreferrer">Help</a>
</div>
<div class="col-auto">
<button type="button" class="btn btn-secondary" (click)="close()">{{t('close')}}</button>
</div>
<div class="col-auto">
<button type="button" class="btn btn-primary" (click)="prevStep()" [disabled]="!canMoveToPrevStep()">{{t('prev')}}</button>
</div>
<div class="col-auto">
<button type="button" class="btn btn-primary" (click)="nextStep()" [disabled]="!canMoveToNextStep()">{{t(NextButtonLabel)}}</button>
</div>
</div>
</ng-container>