Angular 16 (#2007)

* Removed adv, which isn't needed.

* Updated zone

* Updated to angular 16

* Updated to angular 16 (partially)

* Updated to angular 16

* Package update for Angular 16 (and other dependencies) is complete.

* Replaced all takeUntil(this.onDestroy) with new takeUntilDestroyed()

* Updated all inputs that have ! to be required and deleted all unit tests.

* Corrected how takeUntilDestroyed() is supposed to be implemented.
This commit is contained in:
Joe Milazzo 2023-05-21 12:30:32 -05:00 committed by GitHub
parent 9bc8361381
commit 9c06cccd35
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
87 changed files with 3964 additions and 20426 deletions

View file

@ -1,4 +1,13 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
DestroyRef,
inject,
Input,
OnDestroy,
OnInit
} from '@angular/core';
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import { BehaviorSubject, Observable, of, Subject } from 'rxjs';
import { map, shareReplay, takeUntil } from 'rxjs/operators';
@ -13,6 +22,7 @@ import { UpdateVersionEvent } from 'src/app/_models/events/update-version-event'
import { User } from 'src/app/_models/user';
import { AccountService } from 'src/app/_services/account.service';
import { EVENTS, Message, MessageHubService } from 'src/app/_services/message-hub.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({
selector: 'app-nav-events-toggle',
@ -21,12 +31,11 @@ import { EVENTS, Message, MessageHubService } from 'src/app/_services/message-hu
changeDetection: ChangeDetectionStrategy.OnPush
})
export class EventsWidgetComponent implements OnInit, OnDestroy {
@Input() user!: User;
@Input({required: true}) user!: User;
private readonly destroyRef = inject(DestroyRef);
isAdmin$: Observable<boolean> = of(false);
private readonly onDestroy = new Subject<void>();
/**
* Progress events (Event Type: 'started', 'ended', 'updated' that have progress property)
*/
@ -53,21 +62,19 @@ export class EventsWidgetComponent implements OnInit, OnDestroy {
return EVENTS;
}
constructor(public messageHub: MessageHubService, private modalService: NgbModal,
constructor(public messageHub: MessageHubService, private modalService: NgbModal,
private accountService: AccountService, private confirmService: ConfirmService,
private readonly cdRef: ChangeDetectorRef, public downloadService: DownloadService) {
}
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
this.progressEventsSource.complete();
this.singleUpdateSource.complete();
this.errorSource.complete();
}
ngOnInit(): void {
this.messageHub.messages$.pipe(takeUntil(this.onDestroy)).subscribe(event => {
this.messageHub.messages$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(event => {
if (event.event === EVENTS.NotificationProgress) {
this.processNotificationProgressEvent(event);
} else if (event.event === EVENTS.Error) {
@ -86,8 +93,8 @@ export class EventsWidgetComponent implements OnInit, OnDestroy {
});
this.isAdmin$ = this.accountService.currentUser$.pipe(
takeUntil(this.onDestroy),
map(user => (user && this.accountService.hasAdminRole(user)) || false),
takeUntilDestroyed(this.destroyRef),
map(user => (user && this.accountService.hasAdminRole(user)) || false),
shareReplay()
);
}
@ -187,11 +194,11 @@ export class EventsWidgetComponent implements OnInit, OnDestroy {
let data = [];
if (messageEvent.name === EVENTS.Info) {
data = this.infoSource.getValue();
data = data.filter(m => m !== messageEvent);
data = data.filter(m => m !== messageEvent);
this.infoSource.next(data);
} else {
data = this.errorSource.getValue();
data = data.filter(m => m !== messageEvent);
data = data.filter(m => m !== messageEvent);
this.errorSource.next(data);
}
this.activeEvents = Math.max(this.activeEvents - 1, 0);

View file

@ -3,7 +3,7 @@
<div class="search">
<input #input [id]="id" type="text" inputmode="search" autocomplete="off" formControlName="typeahead" [placeholder]="placeholder"
aria-haspopup="listbox" aria-owns="dropdown" aria-expanded="hasFocus && (grouppedData.persons.length || grouppedData.collections.length || grouppedData.series.length || grouppedData.persons.length || grouppedData.tags.length || grouppedData.genres.length)"
aria-autocomplete="list" (focusout)="close($event)" (focus)="open($event)" role="search"
aria-autocomplete="list" (focusout)="close($event)" (focus)="open($event)" role="search"
>
<div class="spinner-border spinner-border-sm" role="status" *ngIf="isLoading">
<span class="visually-hidden">Loading...</span>
@ -13,90 +13,90 @@
</div>
<div class="dropdown" *ngIf="hasFocus">
<ul class="list-group" role="listbox" id="dropdown">
<ng-container *ngIf="seriesTemplate !== undefined && grouppedData.series.length > 0">
<ng-container *ngIf="seriesTemplate !== undefined && groupedData.series.length > 0">
<li class="list-group-item section-header"><h5 id="series-group">Series</h5></li>
<ul class="list-group results" role="group" aria-describedby="series-group">
<li *ngFor="let option of grouppedData.series; let index = index;" (click)="handleResultlick(option)" tabindex="0"
<li *ngFor="let option of groupedData.series; let index = index;" (click)="handleResultlick(option)" tabindex="0"
class="list-group-item" aria-labelledby="series-group" role="option">
<ng-container [ngTemplateOutlet]="seriesTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li>
</ul>
</ng-container>
<ng-container *ngIf="collectionTemplate !== undefined && grouppedData.collections.length > 0">
<ng-container *ngIf="collectionTemplate !== undefined && groupedData.collections.length > 0">
<li class="list-group-item section-header"><h5>Collections</h5></li>
<ul class="list-group results">
<li *ngFor="let option of grouppedData.collections; let index = index;" (click)="handleResultlick(option)" tabindex="0"
<li *ngFor="let option of groupedData.collections; let index = index;" (click)="handleResultlick(option)" tabindex="0"
class="list-group-item" role="option">
<ng-container [ngTemplateOutlet]="collectionTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li>
</ul>
</ng-container>
<ng-container *ngIf="readingListTemplate !== undefined && grouppedData.readingLists.length > 0">
<ng-container *ngIf="readingListTemplate !== undefined && groupedData.readingLists.length > 0">
<li class="list-group-item section-header"><h5>Reading Lists</h5></li>
<ul class="list-group results">
<li *ngFor="let option of grouppedData.readingLists; let index = index;" (click)="handleResultlick(option)" tabindex="0"
<li *ngFor="let option of groupedData.readingLists; let index = index;" (click)="handleResultlick(option)" tabindex="0"
class="list-group-item" role="option">
<ng-container [ngTemplateOutlet]="readingListTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li>
</ul>
</ng-container>
<ng-container *ngIf="libraryTemplate !== undefined && grouppedData.libraries.length > 0">
<ng-container *ngIf="libraryTemplate !== undefined && groupedData.libraries.length > 0">
<li class="list-group-item section-header"><h5 id="libraries-group">Libraries</h5></li>
<ul class="list-group results" role="group" aria-describedby="libraries-group">
<li *ngFor="let option of grouppedData.libraries; let index = index;" (click)="handleResultlick(option)" tabindex="0"
<li *ngFor="let option of groupedData.libraries; let index = index;" (click)="handleResultlick(option)" tabindex="0"
class="list-group-item" aria-labelledby="libraries-group" role="option">
<ng-container [ngTemplateOutlet]="libraryTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li>
</ul>
</ng-container>
<ng-container *ngIf="genreTemplate !== undefined && grouppedData.genres.length > 0">
<ng-container *ngIf="genreTemplate !== undefined && groupedData.genres.length > 0">
<li class="list-group-item section-header"><h5>Genres</h5></li>
<ul class="list-group results">
<li *ngFor="let option of grouppedData.genres; let index = index;" (click)="handleResultlick(option)" tabindex="0"
<li *ngFor="let option of groupedData.genres; let index = index;" (click)="handleResultlick(option)" tabindex="0"
class="list-group-item" role="option">
<ng-container [ngTemplateOutlet]="genreTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li>
</ul>
</ng-container>
<ng-container *ngIf="tagTemplate !== undefined && grouppedData.tags.length > 0">
<ng-container *ngIf="tagTemplate !== undefined && groupedData.tags.length > 0">
<li class="list-group-item section-header"><h5>Tags</h5></li>
<ul class="list-group results">
<li *ngFor="let option of grouppedData.tags; let index = index;" (click)="handleResultlick(option)" tabindex="0"
<li *ngFor="let option of groupedData.tags; let index = index;" (click)="handleResultlick(option)" tabindex="0"
class="list-group-item" role="option">
<ng-container [ngTemplateOutlet]="tagTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li>
</ul>
</ng-container>
<ng-container *ngIf="personTemplate !== undefined && grouppedData.persons.length > 0">
<ng-container *ngIf="personTemplate !== undefined && groupedData.persons.length > 0">
<li class="list-group-item section-header"><h5>People</h5></li>
<ul class="list-group results">
<li *ngFor="let option of grouppedData.persons; let index = index;" (click)="handleResultlick(option)" tabindex="0"
<li *ngFor="let option of groupedData.persons; let index = index;" (click)="handleResultlick(option)" tabindex="0"
class="list-group-item" role="option">
<ng-container [ngTemplateOutlet]="personTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li>
</ul>
</ng-container>
<ng-container *ngIf="chapterTemplate !== undefined && grouppedData.chapters.length > 0">
<ng-container *ngIf="chapterTemplate !== undefined && groupedData.chapters.length > 0">
<li class="list-group-item section-header"><h5>Chapters</h5></li>
<ul class="list-group results">
<li *ngFor="let option of grouppedData.chapters; let index = index;" (click)="handleResultlick(option)" tabindex="0"
<li *ngFor="let option of groupedData.chapters; let index = index;" (click)="handleResultlick(option)" tabindex="0"
class="list-group-item" role="option">
<ng-container [ngTemplateOutlet]="chapterTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li>
</ul>
</ng-container>
<ng-container *ngIf="fileTemplate !== undefined && grouppedData.files.length > 0">
<ng-container *ngIf="fileTemplate !== undefined && groupedData.files.length > 0">
<li class="list-group-item section-header"><h5>Files</h5></li>
<ul class="list-group results">
<li *ngFor="let option of grouppedData.files; let index = index;" (click)="handleResultlick(option)" tabindex="0"
<li *ngFor="let option of groupedData.files; let index = index;" (click)="handleResultlick(option)" tabindex="0"
class="list-group-item" role="option">
<ng-container [ngTemplateOutlet]="fileTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li>

View file

@ -1,9 +1,25 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, ElementRef, EventEmitter, HostListener, Input, OnDestroy, OnInit, Output, TemplateRef, ViewChild } from '@angular/core';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ContentChild, DestroyRef,
ElementRef,
EventEmitter,
HostListener,
inject,
Input,
OnDestroy,
OnInit,
Output,
TemplateRef,
ViewChild
} from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { Subject } from 'rxjs';
import { debounceTime, takeUntil } from 'rxjs/operators';
import { KEY_CODES } from 'src/app/shared/_services/utility.service';
import { SearchResultGroup } from 'src/app/_models/search/search-result-group';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({
selector: 'app-grouped-typeahead',
@ -11,7 +27,7 @@ import { SearchResultGroup } from 'src/app/_models/search/search-result-group';
styleUrls: ['./grouped-typeahead.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class GroupedTypeaheadComponent implements OnInit, OnDestroy {
export class GroupedTypeaheadComponent implements OnInit {
/**
* Unique id to tie with a label element
*/
@ -24,7 +40,7 @@ export class GroupedTypeaheadComponent implements OnInit, OnDestroy {
* Initial value of the search model
*/
@Input() initialValue: string = '';
@Input() grouppedData: SearchResultGroup = new SearchResultGroup();
@Input() groupedData: SearchResultGroup = new SearchResultGroup();
/**
* Placeholder for the input
*/
@ -62,7 +78,8 @@ export class GroupedTypeaheadComponent implements OnInit, OnDestroy {
@ContentChild('readingListTemplate') readingListTemplate!: TemplateRef<any>;
@ContentChild('fileTemplate') fileTemplate!: TemplateRef<any>;
@ContentChild('chapterTemplate') chapterTemplate!: TemplateRef<any>;
private readonly destroyRef = inject(DestroyRef);
hasFocus: boolean = false;
isLoading: boolean = false;
@ -70,16 +87,14 @@ export class GroupedTypeaheadComponent implements OnInit, OnDestroy {
prevSearchTerm: string = '';
private onDestroy: Subject<void> = new Subject();
get searchTerm() {
return this.typeaheadForm.get('typeahead')?.value || '';
}
get hasData() {
return !(this.noResultsTemplate != undefined && !this.grouppedData.persons.length && !this.grouppedData.collections.length
&& !this.grouppedData.series.length && !this.grouppedData.persons.length && !this.grouppedData.tags.length && !this.grouppedData.genres.length && !this.grouppedData.libraries.length
&& !this.grouppedData.files.length && !this.grouppedData.chapters.length);
return !(this.noResultsTemplate != undefined && !this.groupedData.persons.length && !this.groupedData.collections.length
&& !this.groupedData.series.length && !this.groupedData.persons.length && !this.groupedData.tags.length && !this.groupedData.genres.length && !this.groupedData.libraries.length
&& !this.groupedData.files.length && !this.groupedData.chapters.length);
}
@ -92,7 +107,7 @@ export class GroupedTypeaheadComponent implements OnInit, OnDestroy {
}
@HostListener('window:keydown', ['$event'])
handleKeyPress(event: KeyboardEvent) {
handleKeyPress(event: KeyboardEvent) {
if (!this.hasFocus) { return; }
switch(event.key) {
@ -109,7 +124,7 @@ export class GroupedTypeaheadComponent implements OnInit, OnDestroy {
this.typeaheadForm.addControl('typeahead', new FormControl(this.initialValue, []));
this.cdRef.markForCheck();
this.typeaheadForm.valueChanges.pipe(debounceTime(this.debounceTime), takeUntil(this.onDestroy)).subscribe(change => {
this.typeaheadForm.valueChanges.pipe(debounceTime(this.debounceTime), takeUntilDestroyed(this.destroyRef)).subscribe(change => {
const value = this.typeaheadForm.get('typeahead')?.value;
if (value != undefined && value != '' && !this.hasFocus) {
@ -127,17 +142,12 @@ export class GroupedTypeaheadComponent implements OnInit, OnDestroy {
});
}
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
onInputFocus(event: any) {
if (event) {
event.stopPropagation();
event.preventDefault();
}
this.openDropdown();
return this.hasFocus;
}

View file

@ -17,7 +17,7 @@
[minQueryLength]="2"
initialValue=""
placeholder="Search…"
[grouppedData]="searchResults"
[groupedData]="searchResults"
(inputChanged)="onChangeSearch($event)"
(clearField)="clearSearch()"
(focusChanged)="focusUpdate($event)"

View file

@ -1,5 +1,15 @@
import { DOCUMENT } from '@angular/common';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Inject, OnDestroy, OnInit, ViewChild } from '@angular/core';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component, DestroyRef,
ElementRef,
inject,
Inject,
OnDestroy,
OnInit,
ViewChild
} from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
import { fromEvent, Subject } from 'rxjs';
import { debounceTime, distinctUntilChanged, filter, takeUntil, tap } from 'rxjs/operators';
@ -17,6 +27,7 @@ import { ImageService } from 'src/app/_services/image.service';
import { NavService } from 'src/app/_services/nav.service';
import { ScrollService } from 'src/app/_services/scroll.service';
import { SearchService } from 'src/app/_services/search.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({
selector: 'app-nav-header',
@ -24,9 +35,10 @@ import { SearchService } from 'src/app/_services/search.service';
styleUrls: ['./nav-header.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class NavHeaderComponent implements OnInit, OnDestroy {
export class NavHeaderComponent implements OnInit {
@ViewChild('search') searchViewRef!: any;
private readonly destroyRef = inject(DestroyRef);
isLoading = false;
debounceTime = 300;
@ -48,7 +60,6 @@ export class NavHeaderComponent implements OnInit, OnDestroy {
backToTopNeeded = false;
searchFocused: boolean = false;
scrollElem: HTMLElement;
private readonly onDestroy = new Subject<void>();
constructor(public accountService: AccountService, private router: Router, public navService: NavService,
public imageService: ImageService, @Inject(DOCUMENT) private document: Document,
@ -57,7 +68,7 @@ export class NavHeaderComponent implements OnInit, OnDestroy {
}
ngOnInit(): void {
this.scrollService.scrollContainer$.pipe(distinctUntilChanged(), takeUntil(this.onDestroy), tap((scrollContainer) => {
this.scrollService.scrollContainer$.pipe(distinctUntilChanged(), takeUntilDestroyed(this.destroyRef), tap((scrollContainer) => {
if (scrollContainer === 'body' || scrollContainer === undefined) {
this.scrollElem = this.document.body;
fromEvent(this.document.body, 'scroll').pipe(debounceTime(20)).subscribe(() => this.checkBackToTopNeeded(this.document.body));
@ -86,11 +97,6 @@ export class NavHeaderComponent implements OnInit, OnDestroy {
this.cdRef.markForCheck();
}
ngOnDestroy() {
this.onDestroy.next();
this.onDestroy.complete();
}
logout() {
this.accountService.logout();
this.navService.hideNavBar();
@ -109,7 +115,7 @@ export class NavHeaderComponent implements OnInit, OnDestroy {
this.searchTerm = val.trim();
this.cdRef.markForCheck();
this.searchService.search(val.trim()).pipe(takeUntil(this.onDestroy)).subscribe(results => {
this.searchService.search(val.trim()).pipe(takeUntilDestroyed(this.destroyRef)).subscribe(results => {
this.searchResults = results;
this.isLoading = false;
this.cdRef.markForCheck();