add song reporting
This commit is contained in:
@@ -8,8 +8,14 @@
|
||||
}} - {{ getStatus(show) }}"
|
||||
>
|
||||
@if (!useSwiper) {
|
||||
<p>{{ show.public ? 'öffentliche' : 'geschlossene' }} Veranstaltung von
|
||||
<p class="show-meta">{{ show.public ? 'öffentliche' : 'geschlossene' }} Veranstaltung von
|
||||
<app-user-name [userId]="show.owner"></app-user-name>
|
||||
<ng-container *appOwner="show.owner">
|
||||
<app-badge [type]="getPublishedBadgeType(show)">{{ show.published | publishedType }}</app-badge>
|
||||
@if (show.reportedType) {
|
||||
<app-badge [type]="getReportedTypeBadgeType(show)">{{ show.reportedType | reportedType }}</app-badge>
|
||||
}
|
||||
</ng-container>
|
||||
</p>
|
||||
}
|
||||
<div class="head">
|
||||
@@ -98,12 +104,12 @@
|
||||
</app-button>
|
||||
}
|
||||
@if (!show.published) {
|
||||
<app-button (click)="onPublish(true)" [icon]="faPublish">
|
||||
<app-button (click)="onPublish(show, true)" [icon]="faPublish">
|
||||
Veröffentlichen
|
||||
</app-button>
|
||||
}
|
||||
@if (show.published) {
|
||||
<app-button (click)="onPublish(false)" [icon]="faUnpublish">
|
||||
<app-button (click)="onPublish(show, false)" [icon]="faUnpublish">
|
||||
Veröffentlichung zurückziehen
|
||||
</app-button>
|
||||
}
|
||||
@@ -112,6 +118,11 @@
|
||||
Teilen
|
||||
</app-button>
|
||||
}
|
||||
@if (show.published && show.reportedType === 'pending') {
|
||||
<app-button (click)="onReport(show)" [icon]="faReport">
|
||||
Melden
|
||||
</app-button>
|
||||
}
|
||||
@if (!show.published) {
|
||||
<app-button (click)="onChange(show.id)" [icon]="faSliders">
|
||||
Ändern
|
||||
|
||||
@@ -13,6 +13,13 @@
|
||||
min-height: calc(100vh - 60px);
|
||||
}
|
||||
|
||||
.show-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
|
||||
.head {
|
||||
display: flex;
|
||||
|
||||
@@ -1,24 +1,114 @@
|
||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
||||
|
||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {BehaviorSubject, of} from 'rxjs';
|
||||
import {ShowComponent} from './show.component';
|
||||
import {ActivatedRoute, Router} from '@angular/router';
|
||||
import {ShowService} from '../services/show.service';
|
||||
import {SongService} from '../../songs/services/song.service';
|
||||
import {ShowSongService} from '../services/show-song.service';
|
||||
import {DocxService} from '../services/docx.service';
|
||||
import {UserService} from '../../../services/user/user.service';
|
||||
import {MatDialog} from '@angular/material/dialog';
|
||||
import {GuestShowService} from '../../guest/guest-show.service';
|
||||
|
||||
describe('ShowComponent', () => {
|
||||
let component: ShowComponent;
|
||||
let fixture: ComponentFixture<ShowComponent>;
|
||||
let showServiceSpy: jasmine.SpyObj<ShowService>;
|
||||
let showSongServiceSpy: jasmine.SpyObj<ShowSongService>;
|
||||
let dialogSpy: jasmine.SpyObj<MatDialog>;
|
||||
let user$: BehaviorSubject<unknown>;
|
||||
let userId$: BehaviorSubject<string | null>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
void TestBed.configureTestingModule({
|
||||
beforeEach(async () => {
|
||||
showServiceSpy = jasmine.createSpyObj<ShowService>('ShowService', ['update$', 'read$']);
|
||||
showSongServiceSpy = jasmine.createSpyObj<ShowSongService>('ShowSongService', ['list$', 'list']);
|
||||
dialogSpy = jasmine.createSpyObj<MatDialog>('MatDialog', ['open']);
|
||||
user$ = new BehaviorSubject<unknown>({id: 'user-1', role: ['leader']});
|
||||
userId$ = new BehaviorSubject<string | null>('user-1');
|
||||
|
||||
showServiceSpy.read$.and.returnValue(of(null));
|
||||
showServiceSpy.update$.and.resolveTo();
|
||||
showSongServiceSpy.list$.and.returnValue(of([]));
|
||||
showSongServiceSpy.list.and.resolveTo([]);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ShowComponent],
|
||||
providers: [
|
||||
{provide: ActivatedRoute, useValue: {params: of({showId: 'show-1'})}},
|
||||
{provide: ShowService, useValue: showServiceSpy},
|
||||
{provide: SongService, useValue: {list$: () => of([])}},
|
||||
{provide: ShowSongService, useValue: showSongServiceSpy},
|
||||
{provide: DocxService, useValue: {create: jasmine.createSpy('create').and.resolveTo()}},
|
||||
{provide: Router, useValue: {navigateByUrl: jasmine.createSpy('navigateByUrl')}},
|
||||
{
|
||||
provide: UserService,
|
||||
useValue: {
|
||||
user$: user$.asObservable(),
|
||||
userId$: userId$.asObservable(),
|
||||
loggedIn$: () => of(true),
|
||||
},
|
||||
},
|
||||
{provide: MatDialog, useValue: dialogSpy},
|
||||
{provide: GuestShowService, useValue: {share: jasmine.createSpy('share').and.resolveTo('https://example.invalid')}},
|
||||
],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ShowComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
void expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should reset reportedType when unpublishing', async () => {
|
||||
await component.onPublish({id: 'show-1', public: true} as never, false);
|
||||
|
||||
expect(showServiceSpy.update$).toHaveBeenCalledWith('show-1', {published: false, reportedType: null});
|
||||
});
|
||||
|
||||
it('should set not-required for private shows when publishing', async () => {
|
||||
await component.onPublish({id: 'show-1', public: false} as never, true);
|
||||
|
||||
expect(showServiceSpy.update$).toHaveBeenCalledWith('show-1', {published: true, reportedType: 'not-required'});
|
||||
});
|
||||
|
||||
it('should set pending for public shows with reportable CCLI songs', async () => {
|
||||
showSongServiceSpy.list.and.resolveTo([{legalOwner: 'CCLI', legalOwnerId: '123'}] as never);
|
||||
|
||||
await component.onPublish({id: 'show-1', public: true} as never, true);
|
||||
|
||||
expect(showServiceSpy.update$).toHaveBeenCalledWith('show-1', {published: true, reportedType: 'pending'});
|
||||
});
|
||||
|
||||
it('should set not-required for public shows without reportable CCLI songs', async () => {
|
||||
showSongServiceSpy.list.and.resolveTo([{legalOwner: 'CCLI', legalOwnerId: ''}] as never);
|
||||
|
||||
await component.onPublish({id: 'show-1', public: true} as never, true);
|
||||
|
||||
expect(showServiceSpy.update$).toHaveBeenCalledWith('show-1', {published: true, reportedType: 'not-required'});
|
||||
});
|
||||
|
||||
it('should open report dialog with deduplicated reportable songs and mark show as reported', () => {
|
||||
component.showSongs = [
|
||||
{id: 'show-song-1', songId: 'song-1', title: 'Alpha', legalOwner: 'CCLI', legalOwnerId: '123'},
|
||||
{id: 'show-song-2', songId: 'song-1', title: 'Alpha', legalOwner: 'CCLI', legalOwnerId: '123'},
|
||||
{id: 'show-song-3', songId: 'song-2', title: 'Beta', legalOwner: 'other', legalOwnerId: '456'},
|
||||
{id: 'show-song-4', songId: 'song-3', title: 'Gamma', legalOwner: 'CCLI', legalOwnerId: '789'},
|
||||
] as never;
|
||||
dialogSpy.open.and.returnValue({afterClosed: () => of(true)} as never);
|
||||
|
||||
component.onReport({id: 'show-1', order: ['show-song-1', 'show-song-2', 'show-song-3', 'show-song-4']} as never);
|
||||
|
||||
expect(dialogSpy.open).toHaveBeenCalledWith(jasmine.any(Function), {
|
||||
width: '640px',
|
||||
data: {
|
||||
songs: [
|
||||
{title: 'Alpha', ccliNumber: '123'},
|
||||
{title: 'Gamma', ccliNumber: '789'},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(showServiceSpy.update$).toHaveBeenCalledWith('show-1', {reportedType: 'reported'});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
faArrowUpRightFromSquare,
|
||||
faBox,
|
||||
faBoxOpen,
|
||||
faCheck,
|
||||
faChevronRight,
|
||||
faFileDownload,
|
||||
faLock,
|
||||
@@ -50,6 +51,10 @@ import {ButtonComponent} from '../../../widget-modules/components/button/button.
|
||||
import {MatMenu, MatMenuTrigger} from '@angular/material/menu';
|
||||
import {ShowTypePipe} from '../../../widget-modules/pipes/show-type-translater/show-type.pipe';
|
||||
import {UserService} from '../../../services/user/user.service';
|
||||
import {ReportedTypePipe} from '../../../widget-modules/pipes/reported-type-translator/reported-type.pipe';
|
||||
import {BadgeComponent, BadgeType} from '../../../widget-modules/components/badge/badge.component';
|
||||
import {ReportDialogComponent, ReportDialogSong} from '../dialog/report-dialog/report-dialog.component';
|
||||
import {PublishedTypePipe} from '../../../widget-modules/pipes/published-type-translator/published-type.pipe';
|
||||
|
||||
@Component({
|
||||
selector: 'app-show',
|
||||
@@ -79,6 +84,9 @@ import {UserService} from '../../../services/user/user.service';
|
||||
AsyncPipe,
|
||||
DatePipe,
|
||||
ShowTypePipe,
|
||||
ReportedTypePipe,
|
||||
PublishedTypePipe,
|
||||
BadgeComponent,
|
||||
],
|
||||
})
|
||||
export class ShowComponent implements OnInit, OnDestroy {
|
||||
@@ -101,6 +109,7 @@ export class ShowComponent implements OnInit, OnDestroy {
|
||||
|
||||
public faBox = faBox;
|
||||
public faBoxOpen = faBoxOpen;
|
||||
public faReport = faCheck;
|
||||
public faPublish = faUnlock;
|
||||
public faUnpublish = faLock;
|
||||
public faShare = faArrowUpRightFromSquare;
|
||||
@@ -185,8 +194,24 @@ export class ShowComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
public async onPublish(published: boolean): Promise<void> {
|
||||
if (this.showId != null) await this.showService.update$(this.showId, {published});
|
||||
public async onPublish(show: Show, published: boolean): Promise<void> {
|
||||
if (!show.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!published) {
|
||||
await this.showService.update$(show.id, {published: false, reportedType: null});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!show.public) {
|
||||
await this.showService.update$(show.id, {published: true, reportedType: 'not-required'});
|
||||
return;
|
||||
}
|
||||
|
||||
const showSongs = this.showSongs ?? (await this.showSongService.list(show.id));
|
||||
const reportedType = showSongs.some(song => song.legalOwner === 'CCLI' && !!song.legalOwnerId) ? 'pending' : 'not-required';
|
||||
await this.showService.update$(show.id, {published: true, reportedType});
|
||||
}
|
||||
|
||||
public onShare = async (show: Show): Promise<void> => {
|
||||
@@ -194,16 +219,69 @@ export class ShowComponent implements OnInit, OnDestroy {
|
||||
this.dialog.open(ShareDialogComponent, {data: {url, show}});
|
||||
};
|
||||
|
||||
public onReport(show: Show): void {
|
||||
const songs = this.getReportableSongs(show);
|
||||
if (songs.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dialogRef = this.dialog.open(ReportDialogComponent, {
|
||||
width: '640px',
|
||||
data: {songs},
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().pipe(take(1)).subscribe((reported: boolean) => {
|
||||
if (reported) {
|
||||
void this.showService.update$(show.id, {reportedType: 'reported'});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public getStatus(show: Show): string {
|
||||
if (show.published) {
|
||||
return 'veröffentlicht';
|
||||
}
|
||||
if (show.reported) {
|
||||
if (show.reportedType === 'reported') {
|
||||
return 'gemeldet';
|
||||
}
|
||||
return 'entwurf';
|
||||
}
|
||||
|
||||
public getReportedTypeBadgeType(show: Show): BadgeType {
|
||||
switch (show.reportedType) {
|
||||
case 'pending':
|
||||
return 'error';
|
||||
case 'reported':
|
||||
return 'ok';
|
||||
case 'not-required':
|
||||
return 'none';
|
||||
default:
|
||||
return 'none';
|
||||
}
|
||||
}
|
||||
|
||||
public getPublishedBadgeType(show: Show): BadgeType {
|
||||
return show.published ? 'ok' : 'none';
|
||||
}
|
||||
|
||||
private getReportableSongs(show: Show): ReportDialogSong[] {
|
||||
const uniqueSongs = new Map<string, ReportDialogSong>();
|
||||
|
||||
this.orderedShowSongs(show)
|
||||
.filter(song => song.legalOwner === 'CCLI' && !!song.legalOwnerId)
|
||||
.forEach(song => {
|
||||
const key = song.songId || `${song.title}:${song.legalOwnerId}`;
|
||||
if (!uniqueSongs.has(key)) {
|
||||
uniqueSongs.set(key, {
|
||||
title: song.title,
|
||||
ccliNumber: song.legalOwnerId,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(uniqueSongs.values());
|
||||
}
|
||||
|
||||
public async onDownload(): Promise<void> {
|
||||
if (this.showId != null) await this.docxService.create(this.showId);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user