This commit is contained in:
@@ -1,86 +1,33 @@
|
||||
{
|
||||
"root": true,
|
||||
"ignorePatterns": [
|
||||
"projects/**/*"
|
||||
],
|
||||
"ignorePatterns": ["projects/**/*"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"*.ts"
|
||||
],
|
||||
"parserOptions": {
|
||||
"project": [
|
||||
"tsconfig.json"
|
||||
],
|
||||
"createDefaultProgram": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:@angular-eslint/recommended",
|
||||
"plugin:@angular-eslint/template/process-inline-templates",
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
||||
"plugin:prettier/recommended"
|
||||
],
|
||||
"files": ["*.ts"],
|
||||
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:@angular-eslint/recommended", "plugin:@angular-eslint/template/process-inline-templates"],
|
||||
"rules": {
|
||||
"prettier/prettier": [
|
||||
"error",
|
||||
{
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/explicit-member-accessibility": "error",
|
||||
"@angular-eslint/component-selector": [
|
||||
"error",
|
||||
{
|
||||
"prefix": "app",
|
||||
"style": "kebab-case",
|
||||
"type": "element"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/unbound-method": [
|
||||
"off"
|
||||
],
|
||||
"@angular-eslint/directive-selector": [
|
||||
"error",
|
||||
{
|
||||
"type": "attribute",
|
||||
"prefix": "app",
|
||||
"style": "camelCase",
|
||||
"type": "attribute"
|
||||
"style": "camelCase"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.html"
|
||||
],
|
||||
"extends": [
|
||||
"plugin:@angular-eslint/template/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"prettier/prettier": [
|
||||
],
|
||||
"@angular-eslint/component-selector": [
|
||||
"error",
|
||||
{
|
||||
"endOfLine": "auto"
|
||||
"type": "element",
|
||||
"prefix": "app",
|
||||
"style": "kebab-case"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.spec.ts"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/await-thenable": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-floating-promises": "off",
|
||||
"@typescript-eslint/no-unsafe-argument": "off",
|
||||
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||
"@typescript-eslint/no-unsafe-call": "off",
|
||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||
"@typescript-eslint/no-unsafe-return": "off"
|
||||
}
|
||||
"files": ["*.html"],
|
||||
"extends": ["plugin:@angular-eslint/template/recommended", "plugin:@angular-eslint/template/accessibility"],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
26
angular.json
26
angular.json
@@ -22,9 +22,7 @@
|
||||
"base": "dist/wgenerator"
|
||||
},
|
||||
"index": "src/index.html",
|
||||
"polyfills": [
|
||||
"src/polyfills.ts"
|
||||
],
|
||||
"polyfills": ["src/polyfills.ts"],
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"inlineStyleLanguage": "less",
|
||||
"assets": [
|
||||
@@ -40,17 +38,9 @@
|
||||
"src/assets",
|
||||
"src/manifest.webmanifest"
|
||||
],
|
||||
"styles": [
|
||||
"src/custom-theme.scss",
|
||||
"src/styles/styles.less",
|
||||
"src/styles/shadow.less"
|
||||
],
|
||||
"styles": ["src/custom-theme.scss", "src/styles/styles.less", "src/styles/shadow.less"],
|
||||
"scripts": [],
|
||||
"allowedCommonJsDependencies": [
|
||||
"lodash",
|
||||
"docx",
|
||||
"qrcode"
|
||||
]
|
||||
"allowedCommonJsDependencies": ["lodash", "docx", "qrcode"]
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
@@ -99,11 +89,15 @@
|
||||
"options": {
|
||||
"runner": "vitest",
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"setupFiles": [
|
||||
"src/test-vitest.ts"
|
||||
],
|
||||
"setupFiles": ["src/test-vitest.ts"],
|
||||
"runnerConfig": true
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-eslint/builder:lint",
|
||||
"options": {
|
||||
"lintFilePatterns": ["src/**/*.ts", "src/**/*.html"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
4932
package-lock.json
generated
4932
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -50,6 +50,7 @@
|
||||
"@angular/language-service": "^21.2.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.57.0",
|
||||
"@typescript-eslint/parser": "^8.57.0",
|
||||
"angular-eslint": "^21.3.1",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
|
||||
@@ -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>
|
||||
} }
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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});
|
||||
|
||||
|
||||
@@ -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']);
|
||||
|
||||
@@ -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))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 }} → </span>
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -48,14 +48,14 @@ export function createSongSearchComparator(filterValue: string): (a: Song, b: So
|
||||
}
|
||||
|
||||
type SearchAnalysis = {
|
||||
compact: string,
|
||||
tokens: string[],
|
||||
compact: string;
|
||||
tokens: string[];
|
||||
};
|
||||
|
||||
type SearchableSong = {
|
||||
text?: SearchAnalysis,
|
||||
title?: SearchAnalysis,
|
||||
artist?: SearchAnalysis,
|
||||
text?: SearchAnalysis;
|
||||
title?: SearchAnalysis;
|
||||
artist?: SearchAnalysis;
|
||||
};
|
||||
|
||||
const searchableSongCache = new WeakMap<Song, SearchableSong>();
|
||||
|
||||
@@ -34,7 +34,9 @@ describe('UserSessionService', () => {
|
||||
} as never);
|
||||
routerSpy.navigateByUrl.and.resolveTo(true);
|
||||
|
||||
createAuthStateSpy = spyOn<any>(UserSessionService.prototype, 'createAuthState$').and.returnValue(authStateSubject.asObservable() as never);
|
||||
createAuthStateSpy = spyOn(UserSessionService.prototype as UserSessionService & {createAuthState$: () => unknown}, 'createAuthState$').and.returnValue(
|
||||
authStateSubject.asObservable() as never
|
||||
);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [
|
||||
|
||||
@@ -29,8 +29,8 @@ describe('UserSongUsageService', () => {
|
||||
{id: 'show-2', owner: 'user-2', archived: true},
|
||||
]) as never
|
||||
);
|
||||
showSongDataServiceSpy.list$.and.callFake((showId: string) =>
|
||||
(of(showId === 'show-1' ? [{songId: 'song-1'}, {songId: 'song-1'}, {songId: 'song-2'}] : [{songId: 'song-3'}]) as never)
|
||||
showSongDataServiceSpy.list$.and.callFake(
|
||||
(showId: string) => of(showId === 'show-1' ? [{songId: 'song-1'}, {songId: 'song-1'}, {songId: 'song-2'}] : [{songId: 'song-3'}]) as never
|
||||
);
|
||||
dbServiceSpy.doc.and.returnValue({update: jasmine.createSpy('update').and.resolveTo()} as never);
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ export class AddSongComponent {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
});
|
||||
|
||||
const filterValue = this.debouncedFilterValue;
|
||||
return filterValue ? searchSongs(songs, filterValue) : songs;
|
||||
|
||||
@@ -29,9 +29,7 @@ export class FilterComponent {
|
||||
this.filterStore.updateSongFilter({q: this.value});
|
||||
});
|
||||
|
||||
this.valueChanged$
|
||||
.pipe(debounceTime(100), distinctUntilChanged(), takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe(text => void this.applyValueChange(text));
|
||||
this.valueChanged$.pipe(debounceTime(100), distinctUntilChanged(), takeUntilDestroyed(this.destroyRef)).subscribe(text => void this.applyValueChange(text));
|
||||
}
|
||||
|
||||
public valueChange(text: string): void {
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
@if (sections && !fullscreen) {
|
||||
<div (click)="onClick()" [class.chords]="iChordMode !== 'hide'" class="song-text">
|
||||
<div
|
||||
(click)="onClick()"
|
||||
(keydown.enter)="onClick()"
|
||||
(keydown.space)="onClick()"
|
||||
[class.chords]="iChordMode !== 'hide'"
|
||||
class="song-text"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
@if (showSwitch) {
|
||||
<button (click)="onChordClick()" class="menu" mat-icon-button>
|
||||
<fa-icon [icon]="faLines"></fa-icon>
|
||||
@@ -23,7 +31,16 @@
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
} @if (sections && fullscreen) {
|
||||
<div (click)="onClick()" [@songSwitch]="sections" [class.chords]="iChordMode !== 'hide'" class="song-text">
|
||||
<div
|
||||
(click)="onClick()"
|
||||
(keydown.enter)="onClick()"
|
||||
(keydown.space)="onClick()"
|
||||
[@songSwitch]="sections"
|
||||
[class.chords]="iChordMode !== 'hide'"
|
||||
class="song-text"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
@if (showSwitch) {
|
||||
<button (click)="onChordClick()" class="menu" mat-icon-button>
|
||||
<fa-icon [icon]="faLines"></fa-icon>
|
||||
|
||||
@@ -31,9 +31,7 @@ describe('RoleGuard', () => {
|
||||
});
|
||||
|
||||
it('should deny access when there is no current user', async () => {
|
||||
await expectAsync(firstValueFrom(guard.canActivate({data: {requiredRoles: ['leader']}} as never))).toBeResolvedTo(
|
||||
{commands: ['brand', 'new-user']} as never
|
||||
);
|
||||
await expectAsync(firstValueFrom(guard.canActivate({data: {requiredRoles: ['leader']}} as never))).toBeResolvedTo({commands: ['brand', 'new-user']} as never);
|
||||
});
|
||||
|
||||
it('should allow admins regardless of requiredRoles', async () => {
|
||||
|
||||
@@ -125,7 +125,8 @@ function decorateMock<T extends ReturnType<typeof vi.fn>>(mock: T): T & MockFunc
|
||||
configurable: true,
|
||||
get: () => ({
|
||||
argsFor(index: number) {
|
||||
return decorated.mock.calls[index] ?? [];
|
||||
const calls = decorated.mock.calls as unknown[][];
|
||||
return calls[index] ?? [];
|
||||
},
|
||||
mostRecent() {
|
||||
const args = decorated.mock.lastCall ?? [];
|
||||
@@ -146,11 +147,7 @@ function createSpy(name?: string): MockFunction {
|
||||
return spy;
|
||||
}
|
||||
|
||||
function createSpyObj<T>(
|
||||
baseName: string,
|
||||
methodNames: string[] | Record<string, unknown>,
|
||||
propertyValues?: Record<string, unknown>
|
||||
): T {
|
||||
function createSpyObj<T>(baseName: string, methodNames: string[] | Record<string, unknown>, propertyValues?: Record<string, unknown>): T {
|
||||
const result: Record<string, unknown> = {};
|
||||
const methods = Array.isArray(methodNames) ? methodNames : Object.keys(methodNames);
|
||||
|
||||
|
||||
27
src/types/jasmine-compat.d.ts
vendored
27
src/types/jasmine-compat.d.ts
vendored
@@ -1,39 +1,38 @@
|
||||
type UnknownFunction = (...args: unknown[]) => unknown;
|
||||
|
||||
type SpyAnd = {
|
||||
returnValue(value?: unknown): jasmine.Spy;
|
||||
resolveTo(value?: unknown): jasmine.Spy;
|
||||
rejectWith(value?: unknown): jasmine.Spy;
|
||||
callFake(fn: (...args: any[]) => unknown): jasmine.Spy;
|
||||
callFake(fn: UnknownFunction): jasmine.Spy;
|
||||
callThrough(): jasmine.Spy;
|
||||
};
|
||||
|
||||
type SpyCalls = {
|
||||
argsFor(index: number): any[];
|
||||
mostRecent(): {args: any[]};
|
||||
argsFor(index: number): unknown[];
|
||||
mostRecent(): {args: unknown[]};
|
||||
};
|
||||
|
||||
declare global {
|
||||
function spyOn<T = any>(object: T, methodName: any): jasmine.Spy;
|
||||
function spyOn<T extends object, K extends keyof T>(object: T, methodName: K): jasmine.Spy;
|
||||
function expectAsync<T>(value: Promise<T>): {
|
||||
toBeResolvedTo(expected: T): Promise<void>;
|
||||
toBeRejectedWithError(expected?: string | RegExp | Error): Promise<void>;
|
||||
};
|
||||
|
||||
namespace jasmine {
|
||||
type Spy<T extends (...args: any[]) => any = (...args: any[]) => any> = T & ReturnType<typeof import('vitest')['vi']['fn']> & {
|
||||
and: SpyAnd;
|
||||
calls: SpyCalls;
|
||||
};
|
||||
type Spy<T extends UnknownFunction = UnknownFunction> = T &
|
||||
ReturnType<(typeof import('vitest'))['vi']['fn']> & {
|
||||
and: SpyAnd;
|
||||
calls: SpyCalls;
|
||||
};
|
||||
|
||||
type SpyObj<T> = {
|
||||
[K in keyof T]: T[K] extends (...args: any[]) => any ? Spy<T[K]> : T[K];
|
||||
[K in keyof T]: T[K] extends UnknownFunction ? Spy<T[K]> : T[K];
|
||||
};
|
||||
|
||||
function createSpy(name?: string): Spy;
|
||||
function createSpyObj<T>(
|
||||
baseName: string,
|
||||
methodNames: string[] | Record<string, unknown>,
|
||||
propertyValues?: Record<string, unknown>
|
||||
): SpyObj<T>;
|
||||
function createSpyObj<T>(baseName: string, methodNames: string[] | Record<string, unknown>, propertyValues?: Record<string, unknown>): SpyObj<T>;
|
||||
function any(expectedClass: unknown): unknown;
|
||||
function anything(): unknown;
|
||||
function objectContaining<T>(value: Partial<T>): unknown;
|
||||
|
||||
2
src/types/vitest-matchers.d.ts
vendored
2
src/types/vitest-matchers.d.ts
vendored
@@ -1,7 +1,7 @@
|
||||
import 'vitest';
|
||||
|
||||
declare module 'vitest' {
|
||||
interface Assertion<T = any> {
|
||||
interface Assertion<T = unknown> {
|
||||
toBeTrue(): T;
|
||||
toBeFalse(): T;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user