From d907c89eb6738c1f68c08ec12d9e5d4bf7610021 Mon Sep 17 00:00:00 2001 From: benjamin Date: Sun, 15 Mar 2026 12:50:33 +0100 Subject: [PATCH] linting --- .eslintrc.json | 3 +- angular.json | 34 ++++++------ e2e/protractor.conf.js | 32 ----------- e2e/src/app.e2e-spec.ts | 25 --------- e2e/src/app.po.ts | 11 ---- e2e/tsconfig.json | 13 ----- package.json | 3 +- .../presentation/monitor/monitor.component.ts | 9 +++- .../presentation/remote/remote.component.ts | 6 ++- .../share-dialog/share-dialog.component.ts | 6 +-- src/app/modules/shows/edit/edit.component.ts | 13 +++-- .../shows/list/filter/filter.component.ts | 40 ++++++++------ src/app/modules/shows/new/new.component.ts | 14 +++-- src/app/modules/shows/show/show.component.ts | 11 ++-- .../modules/shows/show/song/song.component.ts | 16 +++--- .../songs/services/text-rendering.service.ts | 3 +- .../song-list/filter/filter.component.ts | 30 +++++++---- .../edit/edit-file/edit-file.component.ts | 7 ++- .../edit/edit-song/edit-song.component.ts | 37 ++++++------- .../modules/songs/song/edit/edit.service.ts | 54 ++++++++++++------- .../modules/songs/song/new/new.component.ts | 44 +++++++-------- src/app/modules/songs/song/song.component.ts | 12 ++--- .../modules/user/login/auth-message.pipe.ts | 8 +++ src/app/modules/user/login/login.component.ts | 23 +++++--- src/app/modules/user/new/new.component.html | 3 ++ src/app/modules/user/new/new.component.ts | 35 +++++++----- .../user/password/password.component.ts | 21 +++++--- src/app/services/filter.helper.ts | 16 +++--- src/app/services/user/owner.directive.ts | 9 ++-- src/app/services/user/role.directive.ts | 21 ++++---- .../components/add-song/add-song.component.ts | 6 +-- .../navigation/filter/filter.component.ts | 6 ++- .../song-text/song-text.component.ts | 13 +++-- src/app/widget-modules/guards/role.guard.ts | 7 ++- .../section-type.pipe.ts | 2 + tsconfig.json | 2 +- 36 files changed, 309 insertions(+), 286 deletions(-) delete mode 100644 e2e/protractor.conf.js delete mode 100644 e2e/src/app.e2e-spec.ts delete mode 100644 e2e/src/app.po.ts delete mode 100644 e2e/tsconfig.json diff --git a/.eslintrc.json b/.eslintrc.json index 8beac03..32324a2 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -10,8 +10,7 @@ ], "parserOptions": { "project": [ - "tsconfig.json", - "e2e/tsconfig.json" + "tsconfig.json" ], "createDefaultProgram": true }, diff --git a/angular.json b/angular.json index dbd35a7..734a0bb 100644 --- a/angular.json +++ b/angular.json @@ -45,10 +45,6 @@ "src/styles/shadow.less" ], "scripts": [], - "extractLicenses": false, - "sourceMap": true, - "optimization": false, - "namedChunks": true, "allowedCommonJsDependencies": [ "lodash", "docx", @@ -57,6 +53,13 @@ "browser": "src/main.ts" }, "configurations": { + "development": { + "aot": false, + "extractLicenses": false, + "sourceMap": true, + "optimization": false, + "namedChunks": true + }, "production": { "fileReplacements": [ { @@ -84,18 +87,23 @@ ], "serviceWorker": "ngsw-config.json" } - } + }, + "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "options": { - "buildTarget": "wgenerator:build" + "buildTarget": "wgenerator:build:development" }, "configurations": { + "development": { + "buildTarget": "wgenerator:build:development" + }, "production": { "buildTarget": "wgenerator:build:production" } - } + }, + "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", @@ -127,18 +135,6 @@ "src/**/*.html" ] } - }, - "e2e": { - "builder": "@angular-devkit/build-angular:protractor", - "options": { - "protractorConfig": "e2e/protractor.conf.js", - "devServerTarget": "wgenerator:serve" - }, - "configurations": { - "production": { - "devServerTarget": "wgenerator:serve:production" - } - } } } } diff --git a/e2e/protractor.conf.js b/e2e/protractor.conf.js deleted file mode 100644 index 7c798cf..0000000 --- a/e2e/protractor.conf.js +++ /dev/null @@ -1,32 +0,0 @@ -// @ts-check -// Protractor configuration file, see link for more information -// https://github.com/angular/protractor/blob/master/lib/config.ts - -const { SpecReporter } = require('jasmine-spec-reporter'); - -/** - * @type { import("protractor").Config } - */ -exports.config = { - allScriptsTimeout: 11000, - specs: [ - './src/**/*.e2e-spec.ts' - ], - capabilities: { - browserName: 'chrome' - }, - directConnect: true, - baseUrl: 'http://localhost:4200/', - framework: 'jasmine', - jasmineNodeOpts: { - showColors: true, - defaultTimeoutInterval: 30000, - print: function() {} - }, - onPrepare() { - require('ts-node').register({ - project: require('path').join(__dirname, './tsconfig.json') - }); - jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); - } -}; \ No newline at end of file diff --git a/e2e/src/app.e2e-spec.ts b/e2e/src/app.e2e-spec.ts deleted file mode 100644 index 9129c61..0000000 --- a/e2e/src/app.e2e-spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import {AppPage} from './app.po'; -import {browser, logging} from 'protractor'; - -describe('workspace-project App', () => { - let page: AppPage; - - beforeEach(() => { - page = new AppPage(); - }); - - it('should display welcome message', () => { - void page.navigateTo(); - void expect(page.getTitleText()).toEqual('wgenerator app is running!'); - }); - - afterEach(async () => { - // Assert that there are no errors emitted from the browser - const logs = await browser.manage().logs().get(logging.Type.BROWSER); - void expect(logs).not.toContain( - jasmine.objectContaining({ - level: logging.Level.SEVERE, - } as logging.Entry) - ); - }); -}); diff --git a/e2e/src/app.po.ts b/e2e/src/app.po.ts deleted file mode 100644 index b8498c2..0000000 --- a/e2e/src/app.po.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { browser, by, element } from 'protractor'; - -export class AppPage { - navigateTo() { - return browser.get(browser.baseUrl) as Promise; - } - - getTitleText() { - return element(by.css('app-root .content span')).getText() as Promise; - } -} diff --git a/e2e/tsconfig.json b/e2e/tsconfig.json deleted file mode 100644 index c92199c..0000000 --- a/e2e/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - "outDir": "../out-tsc/e2e", - "module": "commonjs", - "target": "es2018", - "types": [ - "jasmine", - "jasminewd2", - "node" - ] - } -} diff --git a/package.json b/package.json index 2d2e13e..8a423e9 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,8 @@ "version": "1.6", "scripts": { "start": "ng serve", - "build": "ng build", + "build": "ng build --configuration production", + "build:dev": "ng build --configuration development", "deploy": "ng build --configuration production && firebase deploy", "test": "ng test", "lint": "ng lint --fix", diff --git a/src/app/modules/presentation/monitor/monitor.component.ts b/src/app/modules/presentation/monitor/monitor.component.ts index 142de73..bef5cee 100644 --- a/src/app/modules/presentation/monitor/monitor.component.ts +++ b/src/app/modules/presentation/monitor/monitor.component.ts @@ -43,6 +43,7 @@ export class MonitorComponent implements OnInit, OnDestroy { public config$: Observable; public presentationBackground: PresentationBackground = 'none'; private destroy$ = new Subject(); + private songSwitchTimeoutId: ReturnType | null = null; public constructor() { const configService = this.configService; @@ -97,7 +98,10 @@ export class MonitorComponent implements OnInit, OnDestroy { if (this.songId !== presentationSongId) { this.songId = 'empty'; } - setTimeout(() => { + if (this.songSwitchTimeoutId) { + clearTimeout(this.songSwitchTimeoutId); + } + this.songSwitchTimeoutId = setTimeout(() => { this.songId = presentationSongId; this.cRef.markForCheck(); }, 600); @@ -113,6 +117,9 @@ export class MonitorComponent implements OnInit, OnDestroy { } public ngOnDestroy(): void { + if (this.songSwitchTimeoutId) { + clearTimeout(this.songSwitchTimeoutId); + } this.destroy$.next(); this.destroy$.complete(); } diff --git a/src/app/modules/presentation/remote/remote.component.ts b/src/app/modules/presentation/remote/remote.component.ts index e06aa54..1ee1010 100644 --- a/src/app/modules/presentation/remote/remote.component.ts +++ b/src/app/modules/presentation/remote/remote.component.ts @@ -120,9 +120,11 @@ export class RemoteComponent implements OnDestroy { }); this.presentationDynamicCaptionChanged$ - .pipe(debounceTime(1000)) + .pipe(debounceTime(1000), takeUntil(this.destroy$)) .subscribe(_ => void this.showService.update$(_.showId, {presentationDynamicCaption: _.presentationDynamicCaption})); - this.presentationDynamicTextChanged$.pipe(debounceTime(1000)).subscribe(_ => void this.showService.update$(_.showId, {presentationDynamicText: _.presentationDynamicText})); + this.presentationDynamicTextChanged$ + .pipe(debounceTime(1000), takeUntil(this.destroy$)) + .subscribe(_ => void this.showService.update$(_.showId, {presentationDynamicText: _.presentationDynamicText})); } public trackBy(index: number, item: PresentationSong): string { diff --git a/src/app/modules/shows/dialog/share-dialog/share-dialog.component.ts b/src/app/modules/shows/dialog/share-dialog/share-dialog.component.ts index 96467ee..e4004cd 100644 --- a/src/app/modules/shows/dialog/share-dialog/share-dialog.component.ts +++ b/src/app/modules/shows/dialog/share-dialog/share-dialog.component.ts @@ -19,7 +19,7 @@ export interface ShareDialogData { export class ShareDialogComponent { public data = inject(MAT_DIALOG_DATA); - public qrCode: string; + public qrCode = ''; public constructor() { const data = this.data; @@ -35,10 +35,10 @@ export class ShareDialogComponent { light: '#ffffff', }, // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-return - }).then(_ => (this.qrCode = _)); + }).then((qrCode: string) => (this.qrCode = qrCode)); } - public async share() { + public async share(): Promise { if (navigator.clipboard) await navigator.clipboard.writeText(this.data.url); if (navigator.share) diff --git a/src/app/modules/shows/edit/edit.component.ts b/src/app/modules/shows/edit/edit.component.ts index 08c90da..f4750bb 100644 --- a/src/app/modules/shows/edit/edit.component.ts +++ b/src/app/modules/shows/edit/edit.component.ts @@ -87,10 +87,15 @@ export class EditComponent implements OnInit { return; } - await this.showService.update$(this.form.value.id, { - date: Timestamp.fromDate(this.form.value.date), - showType: this.form.value.showType, + const {id, date, showType} = this.form.getRawValue(); + if (!id || !date || !showType) { + return; + } + + await this.showService.update$(id, { + date: Timestamp.fromDate(date), + showType, } as Partial); - await this.router.navigateByUrl(`/shows/${this.form.value.id ?? ''}`); + await this.router.navigateByUrl(`/shows/${id}`); } } diff --git a/src/app/modules/shows/list/filter/filter.component.ts b/src/app/modules/shows/list/filter/filter.component.ts index 72c9378..2a31db2 100644 --- a/src/app/modules/shows/list/filter/filter.component.ts +++ b/src/app/modules/shows/list/filter/filter.component.ts @@ -1,6 +1,7 @@ -import {Component, Input, inject} from '@angular/core'; +import {Component, DestroyRef, Input, inject} from '@angular/core'; import {KeyValue} from '@angular/common'; -import {ReactiveFormsModule, UntypedFormBuilder, UntypedFormGroup} from '@angular/forms'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import {FormBuilder, FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms'; import {FilterValues} from './filter-values'; import {Show} from '../../services/show'; import {ShowService} from '../../services/show.service'; @@ -24,13 +25,18 @@ export class FilterComponent { private showService = inject(ShowService); private userService = inject(UserService); private filterStore = inject(FilterStoreService); + private destroyRef = inject(DestroyRef); @Input() public shows: Show[] = []; public showTypePublic = ShowService.SHOW_TYPE_PUBLIC; public showTypePrivate = ShowService.SHOW_TYPE_PRIVATE; - public filterFormGroup: UntypedFormGroup; + public filterFormGroup: FormGroup<{ + time: FormControl; + owner: FormControl; + showType: FormControl; + }>; public times: KeyValue[] = [ {key: 1, value: 'letzter Monat'}, {key: 3, value: 'letztes Quartal'}, @@ -41,15 +47,15 @@ export class FilterComponent { public owners: {key: string; value: string}[] = []; public constructor() { - const fb = inject(UntypedFormBuilder); + const fb = inject(FormBuilder); this.filterFormGroup = fb.group({ - time: 1, - owner: null, - showType: null, + time: fb.nonNullable.control(1), + owner: fb.control(null), + showType: fb.control(null), }); - this.filterStore.showFilter$.subscribe(filterValues => { + this.filterStore.showFilter$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(filterValues => { this.filterFormGroup.patchValue( { time: filterValues.time, @@ -60,11 +66,13 @@ export class FilterComponent { ); }); - this.filterFormGroup.controls.time.valueChanges.subscribe(_ => this.filterValueChanged('time', (_ as number) ?? 1)); - this.filterFormGroup.controls.owner.valueChanges.subscribe(_ => this.filterValueChanged('owner', (_ as string | null) ?? '')); - this.filterFormGroup.controls.showType.valueChanges.subscribe(_ => this.filterValueChanged('showType', (_ as string | null) ?? '')); + this.filterFormGroup.controls.time.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => this.filterValueChanged('time', value)); + this.filterFormGroup.controls.owner.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => this.filterValueChanged('owner', value ?? '')); + this.filterFormGroup.controls.showType.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => this.filterValueChanged('showType', value ?? '')); - this.owners$().subscribe(owners => (this.owners = owners)); + this.owners$() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(owners => (this.owners = owners)); } public owners$ = (): Observable<{key: string; value: string}[]> => { @@ -85,17 +93,15 @@ export class FilterComponent { this.userService.getUserbyId$(ownerId).pipe( map(user => ({ key: ownerId, - value: user?.name, + value: user?.name ?? ownerId, })) ) ) ); }), - map(owners => { - return owners.sort(dynamicSort('value')); - }), + map(owners => owners.sort(dynamicSort<{key: string; value: string}>('value'))), distinctUntilChanged((left, right) => this.sameOwners(left, right)), - map(_ => _ as {key: string; value: string}[]) + map(owners => owners as {key: string; value: string}[]) ); }; diff --git a/src/app/modules/shows/new/new.component.ts b/src/app/modules/shows/new/new.component.ts index 40b8b2d..c8a0bf1 100644 --- a/src/app/modules/shows/new/new.component.ts +++ b/src/app/modules/shows/new/new.component.ts @@ -3,7 +3,7 @@ import {ShowDataService} from '../services/show-data.service'; import {Observable} from 'rxjs'; import {Show} from '../services/show'; import {ShowService} from '../services/show.service'; -import {ReactiveFormsModule, UntypedFormControl, UntypedFormGroup, Validators} from '@angular/forms'; +import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms'; import {Router} from '@angular/router'; import {faSave} from '@fortawesome/free-solid-svg-icons'; import {CardComponent} from '../../../widget-modules/components/card/card.component'; @@ -46,9 +46,9 @@ export class NewComponent implements OnInit { public shows$: Observable; public showTypePublic = ShowService.SHOW_TYPE_PUBLIC; public showTypePrivate = ShowService.SHOW_TYPE_PRIVATE; - public form: UntypedFormGroup = new UntypedFormGroup({ - date: new UntypedFormControl(null, Validators.required), - showType: new UntypedFormControl(null, Validators.required), + public form = new FormGroup({ + date: new FormControl(null, Validators.required), + showType: new FormControl(null, Validators.required), }); public faSave = faSave; @@ -68,7 +68,11 @@ export class NewComponent implements OnInit { return; } - const id = await this.showService.new$(this.form.value as Partial); + const {date, showType} = this.form.getRawValue(); + const id = await this.showService.new$({ + date, + showType, + } as unknown as Partial); await this.router.navigateByUrl(`/shows/${id ?? ''}`); } } diff --git a/src/app/modules/shows/show/show.component.ts b/src/app/modules/shows/show/show.component.ts index 792b063..c932bdd 100644 --- a/src/app/modules/shows/show/show.component.ts +++ b/src/app/modules/shows/show/show.component.ts @@ -3,6 +3,7 @@ import {filter, map, shareReplay, switchMap, tap} from 'rxjs/operators'; import {ActivatedRoute, Router} from '@angular/router'; import {ShowService} from '../services/show.service'; import {Observable, of, Subscription} from 'rxjs'; +import {take} from 'rxjs/operators'; import {Show} from '../services/show'; import {SongService} from '../../songs/services/song.service'; import {Song} from '../../songs/services/song'; @@ -112,12 +113,13 @@ export class ShowComponent implements OnInit, OnDestroy { public faRestore = faMinimize; public faMaximize = faMaximize; public faNextSong = faChevronRight; - public currentTime: Date; + public currentTime!: Date; private subs: Subscription[] = []; + private clockIntervalId: ReturnType | null = null; public ngOnInit(): void { this.currentTime = new Date(); - setInterval(() => { + this.clockIntervalId = setInterval(() => { this.currentTime = new Date(); }, 10000); this.show$ = this.activatedRoute.params.pipe( @@ -155,6 +157,9 @@ export class ShowComponent implements OnInit, OnDestroy { public ngOnDestroy(): void { this.subs.forEach(_ => _.unsubscribe()); + if (this.clockIntervalId) { + clearInterval(this.clockIntervalId); + } } public onZoomIn() { @@ -172,7 +177,7 @@ export class ShowComponent implements OnInit, OnDestroy { width: '350px', }); - dialogRef.afterClosed().subscribe((archive: boolean) => { + dialogRef.afterClosed().pipe(take(1)).subscribe((archive: boolean) => { if (archive && this.showId != null) void this.showService.update$(this.showId, {archived}); }); } diff --git a/src/app/modules/shows/show/song/song.component.ts b/src/app/modules/shows/show/song/song.component.ts index b260d63..a2d98f2 100644 --- a/src/app/modules/shows/show/song/song.component.ts +++ b/src/app/modules/shows/show/song/song.component.ts @@ -1,8 +1,9 @@ -import {Component, Input, OnInit, ViewChild, inject} from '@angular/core'; +import {Component, DestroyRef, Input, OnInit, ViewChild, inject} from '@angular/core'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; import {ShowSongService} from '../../services/show-song.service'; import {ShowSong} from '../../services/show-song'; import {getScale} from '../../../songs/services/key.helper'; -import {ReactiveFormsModule, UntypedFormControl} from '@angular/forms'; +import {FormControl, ReactiveFormsModule} from '@angular/forms'; import {ChordMode, SongTextComponent} from '../../../../widget-modules/components/song-text/song-text.component'; import {Show} from '../../services/show'; import {faEraser, faPenToSquare, faSave, faTrash} from '@fortawesome/free-solid-svg-icons'; @@ -42,6 +43,7 @@ import {CdkDragHandle} from '@angular/cdk/drag-drop'; }) export class SongComponent implements OnInit { private showSongService = inject(ShowSongService); + private destroyRef = inject(DestroyRef); @Input() public show: Show | null = null; @Input() public showId: string | null = null; @@ -54,11 +56,11 @@ export class SongComponent implements OnInit { public faEdit = faPenToSquare; public faSave = faSave; public faEraser = faEraser; - public keyFormControl: UntypedFormControl = new UntypedFormControl(); + public keyFormControl = new FormControl('', {nonNullable: true}); public iSong: ShowSong | null = null; public edit = false; - public editSongControl = new UntypedFormControl(); - @ViewChild('option') private keyOptions: MatSelect; + public editSongControl = new FormControl(null); + @ViewChild('option') private keyOptions!: MatSelect; @Input() public set showSong(song: ShowSong) { @@ -68,8 +70,8 @@ export class SongComponent implements OnInit { public ngOnInit(): void { if (!this.iSong) return; - this.keyFormControl = new UntypedFormControl(this.iSong.key); - this.keyFormControl.valueChanges.subscribe((value: string) => { + this.keyFormControl = new FormControl(this.iSong.key, {nonNullable: true}); + this.keyFormControl.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => { if (!this.showId || !this.iSong) return; void this.showSongService.update$(this.showId, this.iSong.id, {key: value}); }); diff --git a/src/app/modules/songs/services/text-rendering.service.ts b/src/app/modules/songs/services/text-rendering.service.ts index ea1573c..5b8481c 100644 --- a/src/app/modules/songs/services/text-rendering.service.ts +++ b/src/app/modules/songs/services/text-rendering.service.ts @@ -96,10 +96,11 @@ export class TextRenderingService { return []; } - const indices = { + const indices: Record = { [SectionType.Bridge]: 0, [SectionType.Chorus]: 0, [SectionType.Verse]: 0, + [SectionType.Comment]: 0, }; const sections: Section[] = []; diff --git a/src/app/modules/songs/song-list/filter/filter.component.ts b/src/app/modules/songs/song-list/filter/filter.component.ts index 94971ac..351e200 100644 --- a/src/app/modules/songs/song-list/filter/filter.component.ts +++ b/src/app/modules/songs/song-list/filter/filter.component.ts @@ -1,5 +1,6 @@ -import {Component, Input, inject} from '@angular/core'; -import {ReactiveFormsModule, UntypedFormBuilder, UntypedFormGroup} from '@angular/forms'; +import {Component, DestroyRef, Input, inject} from '@angular/core'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import {FormBuilder, FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms'; import {SongService} from '../../services/song.service'; import {FilterValues} from './filter-values'; import {Song} from '../../services/song'; @@ -22,17 +23,24 @@ import {SongTypePipe} from '../../../../widget-modules/pipes/song-type-translate }) export class FilterComponent { private filterStore = inject(FilterStoreService); + private destroyRef = inject(DestroyRef); - public filterFormGroup: UntypedFormGroup; + public filterFormGroup: FormGroup<{ + q: FormControl; + type: FormControl; + key: FormControl; + legalType: FormControl; + flag: FormControl; + }>; @Input() public songs: Song[] = []; public types = SongService.TYPES; public legalType = SongService.LEGAL_TYPE; public keys = KEYS; public constructor() { - const fb = inject(UntypedFormBuilder); + const fb = inject(FormBuilder); - this.filterFormGroup = fb.group({ + this.filterFormGroup = fb.nonNullable.group({ q: '', type: '', key: '', @@ -40,15 +48,15 @@ export class FilterComponent { flag: '', }); - this.filterStore.songFilter$.subscribe(filterValues => { + this.filterStore.songFilter$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(filterValues => { this.filterFormGroup.patchValue(filterValues, {emitEvent: false}); }); - this.filterFormGroup.controls.q.valueChanges.subscribe(_ => this.filterValueChanged('q', (_ as string) ?? '')); - this.filterFormGroup.controls.key.valueChanges.subscribe(_ => this.filterValueChanged('key', (_ as string) ?? '')); - this.filterFormGroup.controls.type.valueChanges.subscribe(_ => this.filterValueChanged('type', (_ as string) ?? '')); - this.filterFormGroup.controls.legalType.valueChanges.subscribe(_ => this.filterValueChanged('legalType', (_ as string) ?? '')); - this.filterFormGroup.controls.flag.valueChanges.subscribe(_ => this.filterValueChanged('flag', (_ as string) ?? '')); + this.filterFormGroup.controls.q.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => this.filterValueChanged('q', value)); + this.filterFormGroup.controls.key.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => this.filterValueChanged('key', value)); + this.filterFormGroup.controls.type.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => this.filterValueChanged('type', value)); + this.filterFormGroup.controls.legalType.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => this.filterValueChanged('legalType', value)); + this.filterFormGroup.controls.flag.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => this.filterValueChanged('flag', value)); } public getFlags(): string[] { diff --git a/src/app/modules/songs/song/edit/edit-file/edit-file.component.ts b/src/app/modules/songs/song/edit/edit-file/edit-file.component.ts index 8d631ca..f1c0f4c 100644 --- a/src/app/modules/songs/song/edit/edit-file/edit-file.component.ts +++ b/src/app/modules/songs/song/edit/edit-file/edit-file.component.ts @@ -1,4 +1,5 @@ -import {Component, inject} from '@angular/core'; +import {Component, DestroyRef, inject} from '@angular/core'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; import {Upload} from '../../../services/upload'; import {UploadService} from '../../../services/upload.service'; import {ActivatedRoute} from '@angular/router'; @@ -22,6 +23,7 @@ export class EditFileComponent { private activatedRoute = inject(ActivatedRoute); private uploadService = inject(UploadService); private fileService = inject(FileDataService); + private destroyRef = inject(DestroyRef); public selectedFiles: FileList | null = null; public currentUpload: Upload | null = null; @@ -32,7 +34,8 @@ export class EditFileComponent { this.activatedRoute.params .pipe( map(param => param as {songId: string}), - map(param => param.songId) + map(param => param.songId), + takeUntilDestroyed(this.destroyRef) ) .subscribe(songId => { this.songId = songId; diff --git a/src/app/modules/songs/song/edit/edit-song/edit-song.component.ts b/src/app/modules/songs/song/edit/edit-song/edit-song.component.ts index ab09400..74f444f 100644 --- a/src/app/modules/songs/song/edit/edit-song/edit-song.component.ts +++ b/src/app/modules/songs/song/edit/edit-song/edit-song.component.ts @@ -1,9 +1,10 @@ -import {Component, OnInit, inject} from '@angular/core'; +import {Component, DestroyRef, inject, OnInit} from '@angular/core'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; import {Song} from '../../../services/song'; -import {ReactiveFormsModule, UntypedFormGroup} from '@angular/forms'; +import {ReactiveFormsModule} from '@angular/forms'; import {ActivatedRoute, Router, RouterStateSnapshot} from '@angular/router'; import {SongService} from '../../../services/song.service'; -import {EditService} from '../edit.service'; +import {EditService, SongFormGroup} from '../edit.service'; import {first, map, switchMap} from 'rxjs/operators'; import {startWith} from 'rxjs'; import {KEYS} from '../../../services/key.helper'; @@ -62,15 +63,9 @@ import {StatusPipe} from '../../../../../widget-modules/pipes/status-translater/ ], }) export class EditSongComponent implements OnInit { - private activatedRoute = inject(ActivatedRoute); - private songService = inject(SongService); - private editService = inject(EditService); - private router = inject(Router); - private textRenderingService = inject(TextRenderingService); public dialog = inject(MatDialog); - public song: Song | null = null; - public form: UntypedFormGroup = new UntypedFormGroup({}); + public form = {} as SongFormGroup; public keys = KEYS; public types = SongService.TYPES; public status = SongService.STATUS; @@ -83,6 +78,12 @@ export class EditSongComponent implements OnInit { public faLink = faExternalLinkAlt; public songtextFocus = false; public chordValidationIssues: ChordValidationIssue[] = []; + private activatedRoute = inject(ActivatedRoute); + private songService = inject(SongService); + private editService = inject(EditService); + private router = inject(Router); + private textRenderingService = inject(TextRenderingService); + private destroyRef = inject(DestroyRef); public ngOnInit(): void { this.activatedRoute.params @@ -90,23 +91,24 @@ export class EditSongComponent implements OnInit { map(param => param as {songId: string}), map(param => param.songId), switchMap(songId => this.songService.read$(songId)), - first() + first(), + takeUntilDestroyed(this.destroyRef) ) .subscribe(song => { this.song = song; if (!song) return; this.form = this.editService.createSongForm(song); - this.form.controls.flags.valueChanges.subscribe(_ => this.onFlagsChanged(_ as string)); - this.form.controls.text.valueChanges.pipe(startWith(this.form.controls.text.value)).subscribe(text => { - this.updateChordValidation(text as string); + this.form.controls.flags.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => this.onFlagsChanged(value)); + this.form.controls.text.valueChanges.pipe(startWith(this.form.controls.text.value), takeUntilDestroyed(this.destroyRef)).subscribe(text => { + this.updateChordValidation(text); }); - this.onFlagsChanged(this.form.controls.flags.value as string); + this.onFlagsChanged(this.form.controls.flags.value); }); } public async onSave(): Promise { if (!this.song || this.form.invalid) return; - const data = this.form.value as Partial; + const data = this.form.getRawValue() as Partial; await this.songService.update$(this.song.id, data); this.form.markAsPristine(); await this.router.navigateByUrl('songs/' + this.song.id); @@ -121,7 +123,6 @@ export class EditSongComponent implements OnInit { const input = event.input; const value = event.value; - // Add our fruit if ((value || '').trim()) { const flags = [...this.flags, value.trim()]; this.form.controls.flags.setValue(flags.join(';')); @@ -174,7 +175,7 @@ export class EditSongComponent implements OnInit { private async onSaveDialogAfterClosed(save: boolean, url: string) { if (save && this.song && !this.form.invalid) { - const data = this.form.value as Partial; + const data = this.form.getRawValue() as Partial; await this.songService.update$(this.song.id, data); } diff --git a/src/app/modules/songs/song/edit/edit.service.ts b/src/app/modules/songs/song/edit/edit.service.ts index 14afc9c..a00046a 100644 --- a/src/app/modules/songs/song/edit/edit.service.ts +++ b/src/app/modules/songs/song/edit/edit.service.ts @@ -1,30 +1,48 @@ import {Injectable} from '@angular/core'; import {Song} from '../../services/song'; -import {UntypedFormControl, UntypedFormGroup} from '@angular/forms'; +import {FormControl, FormGroup} from '@angular/forms'; + +export type SongFormGroup = FormGroup<{ + text: FormControl; + title: FormControl; + comment: FormControl; + flags: FormControl; + key: FormControl; + tempo: FormControl; + type: FormControl; + status: FormControl; + legalType: FormControl; + legalOwner: FormControl; + legalOwnerId: FormControl; + artist: FormControl; + label: FormControl; + termsOfUse: FormControl; + origin: FormControl; +}>; @Injectable({ providedIn: 'root', }) export class EditService { - public createSongForm(song: Song): UntypedFormGroup { - return new UntypedFormGroup({ - text: new UntypedFormControl(song.text), - title: new UntypedFormControl(song.title), - comment: new UntypedFormControl(song.comment), - flags: new UntypedFormControl(song.flags), - key: new UntypedFormControl(song.key), - tempo: new UntypedFormControl(song.tempo), - type: new UntypedFormControl(song.type), - status: new UntypedFormControl(song.status ?? 'draft'), + public createSongForm(song: Song): SongFormGroup { + return new FormGroup({ + text: new FormControl(song.text, {nonNullable: true}), + title: new FormControl(song.title, {nonNullable: true}), + comment: new FormControl(song.comment, {nonNullable: true}), + flags: new FormControl(song.flags, {nonNullable: true}), + key: new FormControl(song.key, {nonNullable: true}), + tempo: new FormControl(song.tempo, {nonNullable: true}), + type: new FormControl(song.type, {nonNullable: true}), + status: new FormControl(song.status ?? 'draft', {nonNullable: true}), - legalType: new UntypedFormControl(song.legalType), - legalOwner: new UntypedFormControl(song.legalOwner), - legalOwnerId: new UntypedFormControl(song.legalOwnerId), + legalType: new FormControl(song.legalType, {nonNullable: true}), + legalOwner: new FormControl(song.legalOwner, {nonNullable: true}), + legalOwnerId: new FormControl(song.legalOwnerId, {nonNullable: true}), - artist: new UntypedFormControl(song.artist), - label: new UntypedFormControl(song.label), - termsOfUse: new UntypedFormControl(song.termsOfUse), - origin: new UntypedFormControl(song.origin), + artist: new FormControl(song.artist, {nonNullable: true}), + label: new FormControl(song.label, {nonNullable: true}), + termsOfUse: new FormControl(song.termsOfUse, {nonNullable: true}), + origin: new FormControl(song.origin, {nonNullable: true}), }); } } diff --git a/src/app/modules/songs/song/new/new.component.ts b/src/app/modules/songs/song/new/new.component.ts index c4303b6..1936073 100644 --- a/src/app/modules/songs/song/new/new.component.ts +++ b/src/app/modules/songs/song/new/new.component.ts @@ -1,10 +1,11 @@ -import {Component, OnDestroy, OnInit, inject} from '@angular/core'; +import {Component, OnInit, inject} from '@angular/core'; import {faSave} from '@fortawesome/free-solid-svg-icons'; -import {ReactiveFormsModule, UntypedFormControl, UntypedFormGroup, Validators} from '@angular/forms'; +import {DestroyRef} from '@angular/core'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms'; import {SongService} from '../../services/song.service'; import {Song} from '../../services/song'; import {Router} from '@angular/router'; -import {Subscription} from 'rxjs'; import {take} from 'rxjs/operators'; import {CardComponent} from '../../../../widget-modules/components/card/card.component'; import {MatFormField, MatLabel} from '@angular/material/form-field'; @@ -18,39 +19,34 @@ import {ButtonComponent} from '../../../../widget-modules/components/button/butt styleUrls: ['./new.component.less'], imports: [CardComponent, ReactiveFormsModule, MatFormField, MatLabel, MatInput, ButtonRowComponent, ButtonComponent], }) -export class NewComponent implements OnInit, OnDestroy { +export class NewComponent implements OnInit { private songService = inject(SongService); private router = inject(Router); + private destroyRef = inject(DestroyRef); public faSave = faSave; - public form: UntypedFormGroup = new UntypedFormGroup({ - number: new UntypedFormControl(null, Validators.required), - title: new UntypedFormControl(null, Validators.required), + public form = new FormGroup({ + number: new FormControl(null, Validators.required), + title: new FormControl('', {nonNullable: true, validators: [Validators.required]}), }); - private subs: Subscription[] = []; public ngOnInit(): void { this.form.reset(); - this.subs.push( - this.songService - .list$() - .pipe(take(1)) - .subscribe(songs => { - const freeSongnumber = this.getFreeSongNumber(songs); - this.form.controls.number.setValue(freeSongnumber); - }) - ); - } - - public ngOnDestroy(): void { - this.subs.forEach(_ => _.unsubscribe()); + this.songService + .list$() + .pipe(take(1), takeUntilDestroyed(this.destroyRef)) + .subscribe(songs => { + const freeSongnumber = this.getFreeSongNumber(songs); + this.form.controls.number.setValue(freeSongnumber); + }); } public async onSave(): Promise { - const value = this.form.value as {number: number; title: string}; - const songNumber = value.number; - const title = value.title; + const {number: songNumber, title} = this.form.getRawValue(); + if (songNumber == null) { + return; + } const newSongId = await this.songService.new(songNumber, title); await this.router.navigateByUrl('/songs/' + newSongId + '/edit'); } diff --git a/src/app/modules/songs/song/song.component.ts b/src/app/modules/songs/song/song.component.ts index 5f5a088..82c9490 100644 --- a/src/app/modules/songs/song/song.component.ts +++ b/src/app/modules/songs/song/song.component.ts @@ -75,11 +75,12 @@ export class SongComponent implements OnInit { } public ngOnInit(): void { - this.song$ = this.activatedRoute.params.pipe( + const song$ = this.activatedRoute.params.pipe( map(param => param as {songId: string}), map(param => param.songId), switchMap(songId => this.songService.read$(songId)) ); + this.song$ = song$; this.files$ = this.activatedRoute.params.pipe( map(param => param as {songId: string}), @@ -87,7 +88,7 @@ export class SongComponent implements OnInit { switchMap(songId => this.fileService.read$(songId)) ); - this.songCount$ = combineLatest([this.user$, this.song$]).pipe( + this.songCount$ = combineLatest([this.userService.user$, song$]).pipe( map(([user, song]) => { if (!song) { return 0; @@ -111,10 +112,9 @@ export class SongComponent implements OnInit { await this.router.navigateByUrl('/songs'); } - public async addSongToShow(show: Show, song: Song) { - if (!show) return; - const newId = await this.showSongService.new$(show?.id, song.id, false); - await this.showService.update$(show?.id, {order: [...show.order, newId ?? '']}); + public async addSongToShow(show: Show, song: Song): Promise { + const newId = await this.showSongService.new$(show.id, song.id, false); + await this.showService.update$(show.id, {order: [...show.order, newId ?? '']}); await this.router.navigateByUrl('/shows/' + show.id); } } diff --git a/src/app/modules/user/login/auth-message.pipe.ts b/src/app/modules/user/login/auth-message.pipe.ts index 398d6e8..bd9580a 100644 --- a/src/app/modules/user/login/auth-message.pipe.ts +++ b/src/app/modules/user/login/auth-message.pipe.ts @@ -8,6 +8,14 @@ export class AuthMessagePipe implements PipeTransform { return 'Benutzer wurde nicht gefunden'; case 'auth/wrong-password': return 'Passwort ist falsch'; + case 'auth/email-already-in-use': + return 'Die E-Mail-Adresse wird bereits verwendet'; + case 'auth/invalid-email': + return 'Die E-Mail-Adresse ist ungueltig'; + case 'auth/weak-password': + return 'Das Passwort ist zu schwach'; + case 'unknown_error': + return 'Unbekannter Fehler'; default: return code; } diff --git a/src/app/modules/user/login/login.component.ts b/src/app/modules/user/login/login.component.ts index 062b0eb..8a685b8 100644 --- a/src/app/modules/user/login/login.component.ts +++ b/src/app/modules/user/login/login.component.ts @@ -1,5 +1,5 @@ import {Component, OnInit, inject} from '@angular/core'; -import {ReactiveFormsModule, UntypedFormControl, UntypedFormGroup, Validators} from '@angular/forms'; +import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms'; import {Router, RouterLink} from '@angular/router'; import {UserService} from '../../../services/user/user.service'; import {faSignInAlt, faUserPlus} from '@fortawesome/free-solid-svg-icons'; @@ -20,9 +20,9 @@ export class LoginComponent implements OnInit { private userService = inject(UserService); private router = inject(Router); - public form: UntypedFormGroup = new UntypedFormGroup({ - user: new UntypedFormControl(null, [Validators.required, Validators.email]), - pass: new UntypedFormControl(null, [Validators.required]), + public form = new FormGroup({ + user: new FormControl('', {nonNullable: true, validators: [Validators.required, Validators.email]}), + pass: new FormControl('', {nonNullable: true, validators: [Validators.required]}), }); public errorMessage = ''; public faSignIn = faSignInAlt; @@ -36,12 +36,19 @@ export class LoginComponent implements OnInit { this.form.updateValueAndValidity(); if (this.form.valid) { try { - const value = this.form.value as {user: string; pass: string}; - await this.userService.login(value.user, value.pass); + await this.userService.login(this.form.controls.user.value, this.form.controls.pass.value); await this.router.navigateByUrl('/'); - } catch ({code}) { - this.errorMessage = code as string; + } catch (error) { + this.errorMessage = this.errorCode(error); } } } + + private errorCode(error: unknown): string { + if (typeof error === 'object' && error !== null && 'code' in error && typeof error.code === 'string') { + return error.code; + } + + return 'unknown_error'; + } } diff --git a/src/app/modules/user/new/new.component.html b/src/app/modules/user/new/new.component.html index 455c9fc..1004bfc 100644 --- a/src/app/modules/user/new/new.component.html +++ b/src/app/modules/user/new/new.component.html @@ -11,6 +11,9 @@ Passwort + @if (errorMessage) { +

{{ errorMessage | authMessage }}

+ } ('', {nonNullable: true, validators: [Validators.required, Validators.email]}), }); public errorMessage = ''; @@ -36,12 +36,19 @@ export class PasswordComponent implements OnInit { this.form.updateValueAndValidity(); if (this.form.valid) { try { - const value = this.form.value as {user: string}; - await this.userService.changePassword(value.user); + await this.userService.changePassword(this.form.controls.user.value); await this.router.navigateByUrl('/user/password-send'); - } catch ({code}) { - this.errorMessage = code as string; + } catch (error) { + this.errorMessage = this.errorCode(error); } } } + + private errorCode(error: unknown): string { + if (typeof error === 'object' && error !== null && 'code' in error && typeof error.code === 'string') { + return error.code; + } + + return 'unknown_error'; + } } diff --git a/src/app/services/filter.helper.ts b/src/app/services/filter.helper.ts index 0916a4e..d244aef 100644 --- a/src/app/services/filter.helper.ts +++ b/src/app/services/filter.helper.ts @@ -16,17 +16,17 @@ function normalize(input: string): string { export const onlyUnique = (value: T, index: number, array: T[]) => array.indexOf(value) === index; -export function dynamicSort(property: string) { +export function dynamicSort>(property: keyof T | `-${string & keyof T}`) { let sortOrder = 1; - if (property[0] === '-') { + let resolvedProperty = property as string; + if (resolvedProperty[0] === '-') { sortOrder = -1; - property = property.substr(1); + resolvedProperty = resolvedProperty.slice(1); } - return function (a: unknown, b: unknown) { - /* next line works with strings and numbers, - * and you may want to customize it to your needs - */ - const result = a[property] < b[property] ? -1 : a[property] > b[property] ? 1 : 0; + return function (a: T, b: T): number { + const left = a[resolvedProperty as keyof T]; + const right = b[resolvedProperty as keyof T]; + const result = left < right ? -1 : left > right ? 1 : 0; return result * sortOrder; }; } diff --git a/src/app/services/user/owner.directive.ts b/src/app/services/user/owner.directive.ts index 99af7f0..d60dd33 100644 --- a/src/app/services/user/owner.directive.ts +++ b/src/app/services/user/owner.directive.ts @@ -1,12 +1,13 @@ -import {Directive, ElementRef, Input, OnInit, TemplateRef, ViewContainerRef, inject} from '@angular/core'; +import {Directive, DestroyRef, Input, OnInit, TemplateRef, ViewContainerRef, inject} from '@angular/core'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; import {UserService} from './user.service'; @Directive({selector: '[appOwner]'}) export class OwnerDirective implements OnInit { - private element = inject(ElementRef); private templateRef = inject>(TemplateRef); private viewContainer = inject(ViewContainerRef); private userService = inject(UserService); + private destroyRef = inject(DestroyRef); private currentUserId: string | null = null; private iAppOwner: string | null = null; @@ -18,14 +19,14 @@ export class OwnerDirective implements OnInit { } public ngOnInit(): void { - this.userService.userId$.subscribe(user => { + this.userService.userId$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(user => { this.currentUserId = user; this.updateView(); }); this.updateView(); } - private updateView() { + private updateView(): void { this.viewContainer.clear(); if (this.currentUserId === this.iAppOwner) { this.viewContainer.createEmbeddedView(this.templateRef); diff --git a/src/app/services/user/role.directive.ts b/src/app/services/user/role.directive.ts index 280c3fc..f41d195 100644 --- a/src/app/services/user/role.directive.ts +++ b/src/app/services/user/role.directive.ts @@ -1,4 +1,5 @@ -import {ChangeDetectorRef, Directive, ElementRef, Input, OnInit, TemplateRef, ViewContainerRef, inject} from '@angular/core'; +import {ChangeDetectorRef, DestroyRef, Directive, Input, OnInit, TemplateRef, ViewContainerRef, inject} from '@angular/core'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; import {roles} from './roles'; import {UserService} from './user.service'; import {User} from './user'; @@ -6,11 +7,11 @@ import {combineLatest} from 'rxjs'; @Directive({selector: '[appRole]'}) export class RoleDirective implements OnInit { - private element = inject(ElementRef); private templateRef = inject>(TemplateRef); private viewContainer = inject(ViewContainerRef); private userService = inject(UserService); private changeDetection = inject(ChangeDetectorRef); + private destroyRef = inject(DestroyRef); @Input() public appRole: roles[] = []; private currentUser: User | null = null; @@ -18,14 +19,16 @@ export class RoleDirective implements OnInit { private currentViewState = false; public ngOnInit(): void { - combineLatest([this.userService.user$, this.userService.loggedIn$()]).subscribe(_ => { - this.currentUser = _[0]; - this.loggedIn = _[1]; - this.updateView(); - }); + combineLatest([this.userService.user$, this.userService.loggedIn$()]) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(([user, loggedIn]) => { + this.currentUser = user; + this.loggedIn = loggedIn; + this.updateView(); + }); } - private updateView() { + private updateView(): void { const viewState = this.loggedIn && this.checkPermission(); if (this.currentViewState !== viewState) { if (!viewState) this.viewContainer.clear(); @@ -35,7 +38,7 @@ export class RoleDirective implements OnInit { } } - private checkPermission() { + private checkPermission(): boolean { if (this.currentUser && this.currentUser.role) { if (this.currentUser.role === 'admin') { return true; diff --git a/src/app/widget-modules/components/add-song/add-song.component.ts b/src/app/widget-modules/components/add-song/add-song.component.ts index 51a9e1e..09a7a0c 100644 --- a/src/app/widget-modules/components/add-song/add-song.component.ts +++ b/src/app/widget-modules/components/add-song/add-song.component.ts @@ -1,5 +1,5 @@ import {ChangeDetectionStrategy, Component, Input, inject} from '@angular/core'; -import {ReactiveFormsModule, UntypedFormControl} from '@angular/forms'; +import {FormControl, ReactiveFormsModule} from '@angular/forms'; import {filterSong} from '../../../services/filter.helper'; import {MatFormField, MatLabel, MatOption, MatSelect, MatSelectChange} from '@angular/material/select'; import {Song} from '../../../modules/songs/services/song'; @@ -25,7 +25,7 @@ export class AddSongComponent { @Input() public showSongs: ShowSong[] | null = null; @Input() public show: Show | null = null; @Input() public addedLive = false; - public filteredSongsControl = new UntypedFormControl(); + public filteredSongsControl = new FormControl('', {nonNullable: true}); public filteredSongs(): Song[] { if (!this.songs) return []; @@ -44,7 +44,7 @@ export class AddSongComponent { return 0; }); - const filterValue = this.filteredSongsControl.value as string; + const filterValue = this.filteredSongsControl.value; return filterValue ? songs.filter(_ => filterSong(_, filterValue)) : songs; } diff --git a/src/app/widget-modules/components/application-frame/navigation/filter/filter.component.ts b/src/app/widget-modules/components/application-frame/navigation/filter/filter.component.ts index df35713..cb53f30 100644 --- a/src/app/widget-modules/components/application-frame/navigation/filter/filter.component.ts +++ b/src/app/widget-modules/components/application-frame/navigation/filter/filter.component.ts @@ -1,4 +1,5 @@ -import {Component, inject} from '@angular/core'; +import {Component, DestroyRef, inject} from '@angular/core'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; import {ActivatedRoute, Params, Router} from '@angular/router'; import {FormsModule, ReactiveFormsModule} from '@angular/forms'; @@ -10,13 +11,14 @@ import {FormsModule, ReactiveFormsModule} from '@angular/forms'; }) export class FilterComponent { private router = inject(Router); + private destroyRef = inject(DestroyRef); public value = ''; public constructor() { const activatedRoute = inject(ActivatedRoute); - activatedRoute.queryParams.subscribe((params: Params) => { + activatedRoute.queryParams.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((params: Params) => { const typedParams = params as {q: string}; if (typedParams.q) this.value = typedParams.q; }); diff --git a/src/app/widget-modules/components/song-text/song-text.component.ts b/src/app/widget-modules/components/song-text/song-text.component.ts index 644483c..69eacaa 100644 --- a/src/app/widget-modules/components/song-text/song-text.component.ts +++ b/src/app/widget-modules/components/song-text/song-text.component.ts @@ -1,4 +1,4 @@ -import {ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, OnInit, Output, QueryList, ViewChildren, inject} from '@angular/core'; +import {ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output, QueryList, ViewChildren, inject} from '@angular/core'; import {TextRenderingService} from '../../../modules/songs/services/text-rendering.service'; import {faGripLines} from '@fortawesome/free-solid-svg-icons'; import {songSwitch} from './animation'; @@ -27,7 +27,7 @@ interface DisplaySegment { animations: [songSwitch], imports: [MatIconButton, FaIconComponent], }) -export class SongTextComponent implements OnInit { +export class SongTextComponent implements OnInit, OnDestroy { private textRenderingService = inject(TextRenderingService); private elRef = inject>(ElementRef); private cRef = inject(ChangeDetectorRef); @@ -47,6 +47,7 @@ export class SongTextComponent implements OnInit { private invalidChordIssuesByLine = new Map(); private iText = ''; private iTranspose: TransposeMode | null = null; + private offsetIntervalId: ReturnType | null = null; @Input() public set chordMode(value: ChordMode) { @@ -73,7 +74,7 @@ export class SongTextComponent implements OnInit { } public ngOnInit(): void { - setInterval(() => { + this.offsetIntervalId = setInterval(() => { if (!this.fullscreen || this.index === -1 || !this.viewSections?.toArray()[this.index]) { this.offset = 0; this.cRef.markForCheck(); @@ -84,6 +85,12 @@ export class SongTextComponent implements OnInit { }, 100); } + public ngOnDestroy(): void { + if (this.offsetIntervalId) { + clearInterval(this.offsetIntervalId); + } + } + public getLines(section: Section): Line[] { return section.lines.filter(_ => { if (_.type !== LineType.chord) { diff --git a/src/app/widget-modules/guards/role.guard.ts b/src/app/widget-modules/guards/role.guard.ts index 9ca3bd5..ab18f87 100644 --- a/src/app/widget-modules/guards/role.guard.ts +++ b/src/app/widget-modules/guards/role.guard.ts @@ -2,7 +2,7 @@ import {Injectable, inject} from '@angular/core'; import {ActivatedRouteSnapshot, Router, UrlTree} from '@angular/router'; import {Observable} from 'rxjs'; import {UserService} from '../../services/user/user.service'; -import {map} from 'rxjs/operators'; +import {map, take} from 'rxjs/operators'; @Injectable({ providedIn: 'root', @@ -18,8 +18,11 @@ export class RoleGuard { } return this.userService.user$.pipe( + take(1), map(user => { - if (!user) return false; + if (!user) { + return this.router.createUrlTree(['brand', 'new-user']); + } const roles = user.role?.split(';') ?? []; if (roles.indexOf('admin') !== -1) { return true; diff --git a/src/app/widget-modules/pipes/section-type-translator/section-type.pipe.ts b/src/app/widget-modules/pipes/section-type-translator/section-type.pipe.ts index b6a43fa..c1b97f0 100644 --- a/src/app/widget-modules/pipes/section-type-translator/section-type.pipe.ts +++ b/src/app/widget-modules/pipes/section-type-translator/section-type.pipe.ts @@ -13,6 +13,8 @@ export class SectionTypePipe implements PipeTransform { return 'Refrain'; case SectionType.Bridge: return 'Bridge'; + case SectionType.Comment: + return 'Kommentar'; } } } diff --git a/tsconfig.json b/tsconfig.json index 69e0945..13765f8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,7 @@ "moduleResolution": "bundler", "importHelpers": true, "target": "ES2022", - "strict": false, + "strict": true, "typeRoots": ["node_modules/@types"], "useDefineForClassFields": false },