fix linting
Some checks failed
Angular Build / build (push) Has been cancelled

This commit is contained in:
2026-03-20 21:02:02 +01:00
parent d484239429
commit 893a13a8f2
30 changed files with 1936 additions and 3359 deletions

View File

@@ -1,5 +1,4 @@
@if (showState$ | async; as state) {
@if (state.status === 'loaded') {
@if (showState$ | async; as state) { @if (state.status === 'loaded') {
<div class="page">
<div class="title">
<div class="left">{{ state.show.showType | showType }}</div>
@@ -22,16 +21,9 @@
</div>
</div>
} @else if (state.status === 'loading') {
<div class="empty-state">
Gastansicht wird geladen.
</div>
<div class="empty-state">Gastansicht wird geladen.</div>
} @else if (state.status === 'not-found') {
<div class="empty-state">
Für diesen Link wurde keine Gastansicht gefunden.
</div>
<div class="empty-state">Für diesen Link wurde keine Gastansicht gefunden.</div>
} @else {
<div class="empty-state">
{{ state.message }}
</div>
}
}
<div class="empty-state">{{ state.message }}</div>
} }

View File

@@ -74,8 +74,8 @@ export class GuestComponent {
}
if (typeof value === 'object' && value !== null) {
if ('toDate' in value && typeof value.toDate === 'function') {
return value.toDate() as Date;
if (this.hasToDate(value)) {
return value.toDate();
}
if ('seconds' in value && typeof value.seconds === 'number') {
@@ -90,6 +90,10 @@ export class GuestComponent {
return null;
}
private hasToDate(value: object): value is FirestoreDateLike {
return 'toDate' in value && typeof value.toDate === 'function';
}
}
interface GuestShowView extends Omit<GuestShow, 'date' | 'updatedAt'> {
@@ -97,8 +101,8 @@ interface GuestShowView extends Omit<GuestShow, 'date' | 'updatedAt'> {
updatedAt: Date | null;
}
type GuestShowState =
| {status: 'loading'}
| {status: 'not-found'}
| {status: 'error'; message: string}
| {status: 'loaded'; show: GuestShowView};
type GuestShowState = {status: 'loading'} | {status: 'not-found'} | {status: 'error'; message: string} | {status: 'loaded'; show: GuestShowView};
type FirestoreDateLike = {
toDate: () => Date;
};

View File

@@ -4,10 +4,26 @@
<div class="song">
@if (show) {
<div class="song-parts">
<div (click)="onSectionClick('title', -1, show.id)" [class.active]="show.presentationSongId === 'title'" class="song-part">
<div
(click)="onSectionClick('title', -1, show.id)"
(keydown.enter)="onSectionClick('title', -1, show.id)"
(keydown.space)="onSectionClick('title', -1, show.id)"
[class.active]="show.presentationSongId === 'title'"
class="song-part"
role="button"
tabindex="0"
>
<div class="head">Veranstaltung</div>
</div>
<div (click)="onSectionClick('empty', -1, show.id)" [class.active]="show.presentationSongId === 'empty'" class="song-part">
<div
(click)="onSectionClick('empty', -1, show.id)"
(keydown.enter)="onSectionClick('empty', -1, show.id)"
(keydown.space)="onSectionClick('empty', -1, show.id)"
[class.active]="show.presentationSongId === 'empty'"
class="song-part"
role="button"
tabindex="0"
>
<div class="head">Leer</div>
</div>
</div>
@@ -17,18 +33,31 @@
<div class="song">
@if (show) {
<div [class.active]="show.presentationSongId === song.id" class="title song-part">
<div (click)="onSectionClick(song.id, -1, show.id)" class="head">{{ song.title }}</div>
<div
(click)="onSectionClick(song.id, -1, show.id)"
(keydown.enter)="onSectionClick(song.id, -1, show.id)"
(keydown.space)="onSectionClick(song.id, -1, show.id)"
class="head"
role="button"
tabindex="0"
>
{{ song.title }}
</div>
</div>
} @if (show) {
<div class="song-parts">
@for (section of song.sections; track section.type + '-' + section.number + '-' + $index; let i = $index) {
<div
(click)="onSectionClick(song.id, i, show.id)"
(keydown.enter)="onSectionClick(song.id, i, show.id)"
(keydown.space)="onSectionClick(song.id, i, show.id)"
[class.active]="
show.presentationSongId === song.id &&
show.presentationSection === i
"
class="song-part"
role="button"
tabindex="0"
>
<div class="head">{{ section.type | sectionType }} {{ section.number + 1 }}</div>
<div class="fragment">{{ getFirstLine(section) }}</div>
@@ -41,7 +70,16 @@
<div class="song">
@if (show) {
<div [class.active]="show.presentationSongId === 'dynamicText'" class="title song-part">
<div (click)="onSectionClick('dynamicText', -1, show.id)" class="head">Freier Text</div>
<div
(click)="onSectionClick('dynamicText', -1, show.id)"
(keydown.enter)="onSectionClick('dynamicText', -1, show.id)"
(keydown.space)="onSectionClick('dynamicText', -1, show.id)"
class="head"
role="button"
tabindex="0"
>
Freier Text
</div>
</div>
}
<mat-form-field appearance="outline">

View File

@@ -19,11 +19,7 @@ describe('SelectComponent', () => {
routerSpy = jasmine.createSpyObj<Router>('Router', ['navigateByUrl']);
showServiceSpy.list$.and.returnValue(
of([
createShow('older', '2025-12-15T00:00:00Z'),
createShow('recent-a', '2026-03-01T00:00:00Z'),
createShow('recent-b', '2026-02-20T00:00:00Z'),
]) as never
of([createShow('older', '2025-12-15T00:00:00Z'), createShow('recent-a', '2026-03-01T00:00:00Z'), createShow('recent-b', '2026-02-20T00:00:00Z')]) as never
);
showServiceSpy.update$.and.resolveTo();
globalSettingsServiceSpy.set.and.resolveTo();

View File

@@ -5,6 +5,9 @@ import {MAT_DIALOG_DATA} from '@angular/material/dialog';
describe('ShareDialogComponent', () => {
let component: ShareDialogComponent;
let fixture: ComponentFixture<ShareDialogComponent>;
type ShareDialogComponentInternals = ShareDialogComponent & {
generateQrCode: () => Promise<string>;
};
beforeEach(async () => {
await TestBed.configureTestingModule({
@@ -25,7 +28,7 @@ describe('ShareDialogComponent', () => {
fixture = TestBed.createComponent(ShareDialogComponent);
component = fixture.componentInstance;
spyOn<any>(component, 'generateQrCode').and.resolveTo('data:image/jpeg;base64,test');
spyOn(component as ShareDialogComponentInternals, 'generateQrCode').and.resolveTo('data:image/jpeg;base64,test');
fixture.detectChanges();
});

View File

@@ -18,9 +18,7 @@
<app-button [fullWidth]="true" [icon]="faNewShow" routerLink="new">Neue Veranstaltung anlegen </app-button>
</div>
</app-card>
}
@if (publicShows$ | async; as shows) { @if (shows.length > 0) {
} @if (publicShows$ | async; as shows) { @if (shows.length > 0) {
<app-card [padding]="false" heading="Veröffentlichte Veranstaltungen">
@for (show of shows; track trackBy($index, show)) {
<app-list-item [routerLink]="show.id" [show]="show"></app-list-item>

View File

@@ -3,13 +3,25 @@ import {DocxService} from './docx.service';
describe('DocxService', () => {
let service: DocxService;
type PreparedData = {
show: {
showType: string;
date: {toDate: () => Date};
};
songs: unknown[];
user: {name: string};
config: {ccliLicenseId: string};
};
type DocxModuleLike = {
Packer: {toBlob: (document: unknown) => Promise<Blob>};
};
type DocxServiceInternals = DocxService & {
prepareData: (showId: string) => Promise<unknown>;
prepareData: (showId: string) => Promise<PreparedData | null>;
renderTitle: (docx: unknown, title: string) => unknown[];
renderSongs: (docx: unknown, songs: unknown[], options: unknown, config: unknown) => unknown[];
prepareNewDocument: (docx: unknown, data: unknown, options?: unknown, sections?: unknown) => unknown;
saveAs: (blob: Blob, name: string) => void;
loadDocx: () => Promise<{Packer: {toBlob: (document: unknown) => Promise<Blob>}}>;
loadDocx: () => Promise<DocxModuleLike>;
};
beforeEach(async () => {
@@ -23,8 +35,8 @@ describe('DocxService', () => {
it('should not try to save a document when the required data cannot be prepared', async () => {
const serviceInternals = service as DocxServiceInternals;
const prepareDataSpy = spyOn<any>(serviceInternals, 'prepareData').and.resolveTo(null);
const saveAsSpy = spyOn<any>(serviceInternals, 'saveAs');
const prepareDataSpy = spyOn(serviceInternals, 'prepareData').and.resolveTo(null);
const saveAsSpy = spyOn(serviceInternals, 'saveAs');
await service.create('show-1');
@@ -34,13 +46,13 @@ describe('DocxService', () => {
it('should build and save a docx file when all data is available', async () => {
const blob = new Blob(['docx']);
const docxModule = {
const docxModule: DocxModuleLike = {
Packer: {
toBlob: jasmine.createSpy('toBlob').and.resolveTo(blob),
},
};
const serviceInternals = service as DocxServiceInternals;
const prepareDataSpy = spyOn<any>(serviceInternals, 'prepareData').and.resolveTo({
const prepareDataSpy = spyOn(serviceInternals, 'prepareData').and.resolveTo({
show: {
showType: 'service-worship',
date: {toDate: () => new Date('2026-03-10T00:00:00Z')},
@@ -49,11 +61,11 @@ describe('DocxService', () => {
user: {name: 'Benjamin'},
config: {ccliLicenseId: '12345'},
});
spyOn<any>(serviceInternals, 'loadDocx').and.resolveTo(docxModule);
spyOn<any>(serviceInternals, 'renderTitle').and.returnValue([]);
spyOn<any>(serviceInternals, 'renderSongs').and.returnValue([]);
const prepareNewDocumentSpy = spyOn<any>(serviceInternals, 'prepareNewDocument').and.returnValue({doc: true});
const saveAsSpy = spyOn<any>(serviceInternals, 'saveAs');
spyOn(serviceInternals, 'loadDocx').and.resolveTo(docxModule);
spyOn(serviceInternals, 'renderTitle').and.returnValue([]);
spyOn(serviceInternals, 'renderSongs').and.returnValue([]);
const prepareNewDocumentSpy = spyOn(serviceInternals, 'prepareNewDocument').and.returnValue({doc: true});
const saveAsSpy = spyOn(serviceInternals, 'saveAs');
await service.create('show-1', {copyright: true});

View File

@@ -56,10 +56,7 @@ describe('ShowService', () => {
});
it('should not include archived shows from other users when requested', async () => {
shows$.next([
...(shows as unknown as unknown[]),
{id: 'show-4', owner: 'other-user', published: true, archived: true},
]);
shows$.next([...(shows as unknown as unknown[]), {id: 'show-4', owner: 'other-user', published: true, archived: true}]);
const result = await firstValueFrom(service.list$(false, true));
expect(result.map(show => show.id)).toEqual(['show-1', 'show-2', 'show-3']);

View File

@@ -27,9 +27,7 @@ export class ShowService {
(user: User | null, shows: Show[]) => ({user, shows})
),
map(s =>
s.shows
.filter(show => !show.archived || (includeOwnArchived && show.owner === s.user?.id))
.filter(show => show.published || (show.owner === s.user?.id && !publishedOnly))
s.shows.filter(show => !show.archived || (includeOwnArchived && show.owner === s.user?.id)).filter(show => show.published || (show.owner === s.user?.id && !publishedOnly))
)
);
}

View File

@@ -13,7 +13,7 @@
<span class="title">{{ iSong.title }}</span>
@if (!edit) {
<div class="keys-container">
<div (click)="openKeySelect()" class="keys">
<div (click)="openKeySelect()" (keydown.enter)="openKeySelect()" (keydown.space)="openKeySelect()" class="keys" role="button" tabindex="0">
@if (iSong.keyOriginal !== iSong.key) {
<span>{{ iSong.keyOriginal }}&nbsp;&nbsp;</span>
}

View File

@@ -30,7 +30,7 @@ describe('FileService', () => {
});
it('should resolve download urls via AngularFire storage helpers', async () => {
const resolveSpy = spyOn<any>(service as FileServiceInternals, 'resolveDownloadUrl').and.resolveTo('https://cdn.example/file.pdf');
const resolveSpy = spyOn(service as FileServiceInternals, 'resolveDownloadUrl').and.resolveTo('https://cdn.example/file.pdf');
await expectAsync(service.getDownloadUrl('songs/song-1/file.pdf').toPromise()).toBeResolvedTo('https://cdn.example/file.pdf');
@@ -38,7 +38,7 @@ describe('FileService', () => {
});
it('should delete the file from storage and metadata from firestore', async () => {
const deleteFromStorageSpy = spyOn<any>(service as FileServiceInternals, 'deleteFromStorage').and.resolveTo();
const deleteFromStorageSpy = spyOn(service as FileServiceInternals, 'deleteFromStorage').and.resolveTo();
await service.delete('songs/song-1/file.pdf', 'song-1', 'file-1');

View File

@@ -478,13 +478,15 @@ Text`;
const service: TextRenderingService = TestBed.inject(TextRenderingService);
const text = 'Strophe\nC\tG\ta\nText';
void expect(service.validateChordNotation(text)).toEqual(expect.arrayContaining([
jasmine.objectContaining({
lineNumber: 2,
token: '\t',
reason: 'tab_character',
}),
]));
void expect(service.validateChordNotation(text)).toEqual(
expect.arrayContaining([
jasmine.objectContaining({
lineNumber: 2,
token: '\t',
reason: 'tab_character',
}),
])
);
});
it('should not flag tabs on non chord lines', () => {

View File

@@ -39,7 +39,7 @@ describe('UploadService', () => {
success();
},
};
const uploadSpy = spyOn<any>(service as UploadServiceInternals, 'startUpload').and.returnValue(task);
const uploadSpy = spyOn(service as UploadServiceInternals, 'startUpload').and.returnValue(task);
const upload = new Upload(new File(['content'], 'test.pdf', {type: 'application/pdf'}));
service.pushUpload('song-1', upload);

View File

@@ -6,7 +6,7 @@
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Titel</mat-label>
<input autofocus formControlName="title" matInput />
<input formControlName="title" matInput />
</mat-form-field>
</div>

View File

@@ -3,7 +3,7 @@ import {roles} from '../../../services/user/roles';
@Pipe({name: 'role'})
export class RolePipe implements PipeTransform {
public transform(role: roles | string): string {
public transform(role: roles): string {
switch (role) {
case 'contributor':
return 'Mitarbeiter';

View File

@@ -17,7 +17,7 @@
</button>
</div>
} @if (!edit) {
<div (click)="edit = true" class="users list-item">
<div (click)="edit = true" (keydown.enter)="edit = true" (keydown.space)="edit = true" class="users list-item" role="button" tabindex="0">
<span>{{ name }}</span>
<span
>@for (role of roles; track role) {