This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user