add song reporting
This commit is contained in:
@@ -0,0 +1,44 @@
|
|||||||
|
<div mat-dialog-content>
|
||||||
|
<p>
|
||||||
|
Bitte melde die in dieser Veranstaltung verwendeten CCLI-Titel. Die Meldung ist Teil der CCLI-Lizenz und sorgt dafür, dass Songwriter und Verlage korrekt vergütet werden.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Die Meldung erfolgt über
|
||||||
|
<a [href]="reportingUrl" rel="noreferrer" target="_blank">{{ reportingUrl }}</a>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="song-list">
|
||||||
|
<div class="list-head">
|
||||||
|
<div>Titel</div>
|
||||||
|
<div>CCLI-Nummer</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@for (song of data.songs; track song.title + song.ccliNumber) {
|
||||||
|
<div class="list-item">
|
||||||
|
<div>{{ song.title }}</div>
|
||||||
|
<div class="number-cell">
|
||||||
|
<span>{{ song.ccliNumber }}</span>
|
||||||
|
<a
|
||||||
|
(click)="markOpened(song.ccliNumber)"
|
||||||
|
[attr.aria-label]="'CCLI-Titel melden: ' + song.title"
|
||||||
|
[href]="getSongReportingUrl(song.ccliNumber)"
|
||||||
|
rel="noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
class="btn-icon report-link"
|
||||||
|
>
|
||||||
|
<fa-icon [icon]="faOpen"></fa-icon>
|
||||||
|
</a>
|
||||||
|
@if (wasOpened(song.ccliNumber)) {
|
||||||
|
<fa-icon [icon]="faCheck" class="opened-check"></fa-icon>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div mat-dialog-actions>
|
||||||
|
<button [mat-dialog-close]="false" mat-button>Abbrechen</button>
|
||||||
|
<button [mat-dialog-close]="true" cdkFocusInitial mat-button>
|
||||||
|
Alle CCLI-Titel wurden gemeldet
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
.song-list {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-head,
|
||||||
|
.list-item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 180px;
|
||||||
|
gap: 0;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
border-bottom: 1px solid var(--overlay)
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-head {
|
||||||
|
padding: 3px 10px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item {
|
||||||
|
padding: 3px 10px;
|
||||||
|
transition: var(--transition);
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
border-bottom: 1px solid var(--divider)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-head > div,
|
||||||
|
.list-item > div {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.number-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opened-check {
|
||||||
|
color: var(--success);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 640px) {
|
||||||
|
.list-head,
|
||||||
|
.list-item {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-head {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item {
|
||||||
|
padding: 10px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.number-cell {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
import {MAT_DIALOG_DATA} from '@angular/material/dialog';
|
||||||
|
import {ReportDialogComponent} from './report-dialog.component';
|
||||||
|
|
||||||
|
describe('ReportDialogComponent', () => {
|
||||||
|
let component: ReportDialogComponent;
|
||||||
|
let fixture: ComponentFixture<ReportDialogComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ReportDialogComponent],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: MAT_DIALOG_DATA,
|
||||||
|
useValue: {songs: [{title: 'Amazing Grace', ccliNumber: '12345'}]},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ReportDialogComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('create an instance', () => {
|
||||||
|
void expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should build direct reporting urls', () => {
|
||||||
|
expect(component.getSongReportingUrl('5770492')).toBe('https://reporting.ccli.com/search?s=5770492');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should mark numbers as opened locally', () => {
|
||||||
|
expect(component.wasOpened('12345')).toBeFalse();
|
||||||
|
|
||||||
|
component.markOpened('12345');
|
||||||
|
|
||||||
|
expect(component.wasOpened('12345')).toBeTrue();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import {Component, inject} from '@angular/core';
|
||||||
|
import {MAT_DIALOG_DATA, MatDialogActions, MatDialogClose, MatDialogContent} from '@angular/material/dialog';
|
||||||
|
import {MatButton} from '@angular/material/button';
|
||||||
|
import {FaIconComponent} from '@fortawesome/angular-fontawesome';
|
||||||
|
import {faArrowUpRightFromSquare, faCheck} from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
|
export interface ReportDialogSong {
|
||||||
|
title: string;
|
||||||
|
ccliNumber: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReportDialogData {
|
||||||
|
songs: ReportDialogSong[];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-report-dialog',
|
||||||
|
imports: [MatButton, MatDialogActions, MatDialogContent, MatDialogClose, FaIconComponent],
|
||||||
|
templateUrl: './report-dialog.component.html',
|
||||||
|
styleUrl: './report-dialog.component.less',
|
||||||
|
standalone: true,
|
||||||
|
})
|
||||||
|
export class ReportDialogComponent {
|
||||||
|
public readonly reportingUrl = 'https://reporting.ccli.com/search';
|
||||||
|
public readonly faOpen = faArrowUpRightFromSquare;
|
||||||
|
public readonly faCheck = faCheck;
|
||||||
|
public data = inject<ReportDialogData>(MAT_DIALOG_DATA);
|
||||||
|
private readonly openedNumbers = new Set<string>();
|
||||||
|
|
||||||
|
public getSongReportingUrl(ccliNumber: string): string {
|
||||||
|
return `${this.reportingUrl}?s=${encodeURIComponent(ccliNumber)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public markOpened(ccliNumber: string): void {
|
||||||
|
this.openedNumbers.add(ccliNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
public wasOpened(ccliNumber: string): boolean {
|
||||||
|
return this.openedNumbers.has(ccliNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,5 +5,10 @@
|
|||||||
<app-user-name [userId]="show.owner"></app-user-name>
|
<app-user-name [userId]="show.owner"></app-user-name>
|
||||||
</div>
|
</div>
|
||||||
<div>{{ show.showType | showType }}</div>
|
<div>{{ show.showType | showType }}</div>
|
||||||
|
<div>
|
||||||
|
@if (showStatusBadge) {
|
||||||
|
<app-badge [type]="showStatusBadgeType">{{ showStatusBadge }}</app-badge>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.list-item {
|
.list-item {
|
||||||
padding: 5px 20px;
|
padding: 5px 20px;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 100px 150px auto;
|
grid-template-columns: 100px 150px auto 160px;
|
||||||
min-height: 21px;
|
min-height: 21px;
|
||||||
|
|
||||||
& > div {
|
& > div {
|
||||||
|
|||||||
@@ -1,24 +1,44 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
import {BehaviorSubject, of} from 'rxjs';
|
||||||
import {ListItemComponent} from './list-item.component';
|
import {ListItemComponent} from './list-item.component';
|
||||||
|
import {UserService} from '../../../../services/user/user.service';
|
||||||
|
|
||||||
describe('ListItemComponent', () => {
|
describe('ListItemComponent', () => {
|
||||||
let component: ListItemComponent;
|
let component: ListItemComponent;
|
||||||
let fixture: ComponentFixture<ListItemComponent>;
|
let fixture: ComponentFixture<ListItemComponent>;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [ListItemComponent],
|
imports: [ListItemComponent],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: UserService,
|
||||||
|
useValue: {
|
||||||
|
user$: new BehaviorSubject<unknown>({id: 'user-1'}).asObservable(),
|
||||||
|
userId$: new BehaviorSubject<string | null>('user-1').asObservable(),
|
||||||
|
loggedIn$: () => of(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
fixture = TestBed.createComponent(ListItemComponent);
|
fixture = TestBed.createComponent(ListItemComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create', () => {
|
it('should create', () => {
|
||||||
void expect(component).toBeTruthy();
|
void expect(component).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should render a status badge when provided', () => {
|
||||||
|
component.show = {date: {toDate: () => new Date('2026-03-15')} as never, owner: 'user-1', showType: 'misc-private'} as never;
|
||||||
|
component.showStatusBadge = 'nicht gemeldet';
|
||||||
|
component.showStatusBadgeType = 'error';
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const badge = fixture.nativeElement.querySelector('app-badge .badge');
|
||||||
|
expect(badge?.textContent?.trim()).toBe('nicht gemeldet');
|
||||||
|
expect(badge?.className).toContain('error');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,13 +3,16 @@ import {Show} from '../../services/show';
|
|||||||
import {DatePipe} from '@angular/common';
|
import {DatePipe} from '@angular/common';
|
||||||
import {UserNameComponent} from '../../../../services/user/user-name/user-name.component';
|
import {UserNameComponent} from '../../../../services/user/user-name/user-name.component';
|
||||||
import {ShowTypePipe} from '../../../../widget-modules/pipes/show-type-translater/show-type.pipe';
|
import {ShowTypePipe} from '../../../../widget-modules/pipes/show-type-translater/show-type.pipe';
|
||||||
|
import {BadgeComponent, BadgeType} from '../../../../widget-modules/components/badge/badge.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-list-item',
|
selector: 'app-list-item',
|
||||||
templateUrl: './list-item.component.html',
|
templateUrl: './list-item.component.html',
|
||||||
styleUrls: ['./list-item.component.less'],
|
styleUrls: ['./list-item.component.less'],
|
||||||
imports: [UserNameComponent, DatePipe, ShowTypePipe],
|
imports: [UserNameComponent, DatePipe, ShowTypePipe, BadgeComponent],
|
||||||
})
|
})
|
||||||
export class ListItemComponent {
|
export class ListItemComponent {
|
||||||
@Input() public show: Show | null = null;
|
@Input() public show: Show | null = null;
|
||||||
|
@Input() public showStatusBadge: string | null = null;
|
||||||
|
@Input() public showStatusBadgeType: BadgeType = 'none';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
@for (show of shows | sortBy: 'desc':'date'; track trackBy($index, show)) {
|
@for (show of shows | sortBy: 'desc':'date'; track trackBy($index, show)) {
|
||||||
<app-list-item
|
<app-list-item
|
||||||
[routerLink]="show.id"
|
[routerLink]="show.id"
|
||||||
|
[showStatusBadge]="show.published ? 'nicht gemeldet' : 'unveröffentlicht'"
|
||||||
|
[showStatusBadgeType]="show.published ? 'error' : 'none'"
|
||||||
[show]="show"
|
[show]="show"
|
||||||
></app-list-item>
|
></app-list-item>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,73 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
import {BehaviorSubject, of} from 'rxjs';
|
||||||
import {ListComponent} from './list.component';
|
import {ListComponent} from './list.component';
|
||||||
|
import {ShowService} from '../services/show.service';
|
||||||
|
import {UserService} from '../../../services/user/user.service';
|
||||||
|
import {FilterStoreService} from '../../../services/filter-store.service';
|
||||||
|
|
||||||
describe('ListComponent', () => {
|
describe('ListComponent', () => {
|
||||||
let component: ListComponent;
|
let component: ListComponent;
|
||||||
let fixture: ComponentFixture<ListComponent>;
|
let fixture: ComponentFixture<ListComponent>;
|
||||||
|
let shows$: BehaviorSubject<unknown[]>;
|
||||||
|
let user$: BehaviorSubject<unknown>;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({
|
shows$ = new BehaviorSubject<unknown[]>([]);
|
||||||
|
user$ = new BehaviorSubject<unknown>({id: 'user-1'});
|
||||||
|
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
imports: [ListComponent],
|
imports: [ListComponent],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: ShowService,
|
||||||
|
useValue: {
|
||||||
|
list$: () => shows$.asObservable(),
|
||||||
|
listPublicSince$: () => of([]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: UserService,
|
||||||
|
useValue: {
|
||||||
|
user$: user$.asObservable(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
FilterStoreService,
|
||||||
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
fixture = TestBed.createComponent(ListComponent);
|
fixture = TestBed.createComponent(ListComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create', () => {
|
it('should create', () => {
|
||||||
void expect(component).toBeTruthy();
|
void expect(component).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should list own drafts and pending published shows in my shows', done => {
|
||||||
|
shows$.next([
|
||||||
|
{id: 'draft-own', owner: 'user-1', published: false, reportedType: null},
|
||||||
|
{id: 'pending-own', owner: 'user-1', published: true, reportedType: 'pending'},
|
||||||
|
{id: 'reported-own', owner: 'user-1', published: true, reportedType: 'reported'},
|
||||||
|
{id: 'draft-other', owner: 'user-2', published: false, reportedType: null},
|
||||||
|
] as never);
|
||||||
|
|
||||||
|
component.privateShows$.subscribe(shows => {
|
||||||
|
expect(shows.map(show => show.id)).toEqual(['draft-own', 'pending-own']);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore show filters for my shows', done => {
|
||||||
|
const filterStore = TestBed.inject(FilterStoreService);
|
||||||
|
filterStore.updateShowFilter({time: 0, showType: 'service-worship'});
|
||||||
|
shows$.next([
|
||||||
|
{id: 'older-draft', owner: 'user-1', published: false, reportedType: null, showType: 'misc-private'},
|
||||||
|
{id: 'pending-own', owner: 'user-1', published: true, reportedType: 'pending', showType: 'home-group'},
|
||||||
|
] as never);
|
||||||
|
|
||||||
|
component.privateShows$.subscribe(shows => {
|
||||||
|
expect(shows.map(show => show.id)).toEqual(['older-draft', 'pending-own']);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {FilterComponent} from './filter/filter.component';
|
|||||||
import {CardComponent} from '../../../widget-modules/components/card/card.component';
|
import {CardComponent} from '../../../widget-modules/components/card/card.component';
|
||||||
import {ListItemComponent} from './list-item/list-item.component';
|
import {ListItemComponent} from './list-item/list-item.component';
|
||||||
import {SortByPipe} from '../../../widget-modules/pipes/sort-by/sort-by.pipe';
|
import {SortByPipe} from '../../../widget-modules/pipes/sort-by/sort-by.pipe';
|
||||||
|
import {UserService} from '../../../services/user/user.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-list',
|
selector: 'app-list',
|
||||||
@@ -25,14 +26,19 @@ import {SortByPipe} from '../../../widget-modules/pipes/sort-by/sort-by.pipe';
|
|||||||
export class ListComponent {
|
export class ListComponent {
|
||||||
private showService = inject(ShowService);
|
private showService = inject(ShowService);
|
||||||
private filterStore = inject(FilterStoreService);
|
private filterStore = inject(FilterStoreService);
|
||||||
|
private userService = inject(UserService);
|
||||||
|
|
||||||
public filter$ = this.filterStore.showFilter$;
|
public filter$ = this.filterStore.showFilter$;
|
||||||
public lastMonths$ = this.filter$.pipe(map((filterValues: FilterValues) => filterValues.time || 1));
|
public lastMonths$ = this.filter$.pipe(map((filterValues: FilterValues) => filterValues.time || 1));
|
||||||
public owner$ = this.filter$.pipe(map((filterValues: FilterValues) => filterValues.owner));
|
public owner$ = this.filter$.pipe(map((filterValues: FilterValues) => filterValues.owner));
|
||||||
public showType$ = this.filter$.pipe(map((filterValues: FilterValues) => filterValues.showType));
|
public showType$ = this.filter$.pipe(map((filterValues: FilterValues) => filterValues.showType));
|
||||||
public shows$ = this.showService.list$();
|
public shows$ = this.showService.list$();
|
||||||
public privateShows$ = combineLatest([this.shows$, this.filter$]).pipe(
|
public privateShows$ = combineLatest([this.shows$, this.userService.user$]).pipe(
|
||||||
map(([shows, filter]) => shows.filter(show => !show.published).filter(show => this.matchesPrivateFilter(show, filter)))
|
map(([shows, user]) =>
|
||||||
|
shows
|
||||||
|
.filter(show => show.owner === user?.id)
|
||||||
|
.filter(show => !show.published || show.reportedType === 'pending')
|
||||||
|
)
|
||||||
);
|
);
|
||||||
public queriedPublicShows$ = this.lastMonths$.pipe(switchMap(lastMonths => this.showService.listPublicSince$(lastMonths)));
|
public queriedPublicShows$ = this.lastMonths$.pipe(switchMap(lastMonths => this.showService.listPublicSince$(lastMonths)));
|
||||||
public fallbackPublicShows$ = combineLatest([this.shows$, this.lastMonths$]).pipe(
|
public fallbackPublicShows$ = combineLatest([this.shows$, this.lastMonths$]).pipe(
|
||||||
@@ -50,10 +56,6 @@ export class ListComponent {
|
|||||||
|
|
||||||
public trackBy = (index: number, show: unknown) => (show as Show).id;
|
public trackBy = (index: number, show: unknown) => (show as Show).id;
|
||||||
|
|
||||||
private matchesPrivateFilter(show: Show, filter: FilterValues): boolean {
|
|
||||||
return this.matchesTimeFilter(show, filter.time || 1) && (!filter.showType || show.showType === filter.showType);
|
|
||||||
}
|
|
||||||
|
|
||||||
private matchesTimeFilter(show: Show, lastMonths: number): boolean {
|
private matchesTimeFilter(show: Show, lastMonths: number): boolean {
|
||||||
const startDate = new Date();
|
const startDate = new Date();
|
||||||
startDate.setHours(0, 0, 0, 0);
|
startDate.setHours(0, 0, 0, 0);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {Timestamp} from '@angular/fire/firestore';
|
import {Timestamp} from '@angular/fire/firestore';
|
||||||
|
|
||||||
export type PresentationBackground = 'none' | 'blue' | 'green' | 'leder' | 'praise' | 'bible';
|
export type PresentationBackground = 'none' | 'blue' | 'green' | 'leder' | 'praise' | 'bible';
|
||||||
|
export type ReportedType = null | 'pending' | 'reported' | 'not-required';
|
||||||
|
|
||||||
export interface Show {
|
export interface Show {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -9,7 +10,7 @@ export interface Show {
|
|||||||
owner: string;
|
owner: string;
|
||||||
songIds?: string[];
|
songIds?: string[];
|
||||||
public: boolean;
|
public: boolean;
|
||||||
reported: boolean;
|
reportedType: ReportedType;
|
||||||
published: boolean;
|
published: boolean;
|
||||||
archived: boolean;
|
archived: boolean;
|
||||||
order: string[];
|
order: string[];
|
||||||
|
|||||||
@@ -8,8 +8,14 @@
|
|||||||
}} - {{ getStatus(show) }}"
|
}} - {{ getStatus(show) }}"
|
||||||
>
|
>
|
||||||
@if (!useSwiper) {
|
@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>
|
<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>
|
</p>
|
||||||
}
|
}
|
||||||
<div class="head">
|
<div class="head">
|
||||||
@@ -98,12 +104,12 @@
|
|||||||
</app-button>
|
</app-button>
|
||||||
}
|
}
|
||||||
@if (!show.published) {
|
@if (!show.published) {
|
||||||
<app-button (click)="onPublish(true)" [icon]="faPublish">
|
<app-button (click)="onPublish(show, true)" [icon]="faPublish">
|
||||||
Veröffentlichen
|
Veröffentlichen
|
||||||
</app-button>
|
</app-button>
|
||||||
}
|
}
|
||||||
@if (show.published) {
|
@if (show.published) {
|
||||||
<app-button (click)="onPublish(false)" [icon]="faUnpublish">
|
<app-button (click)="onPublish(show, false)" [icon]="faUnpublish">
|
||||||
Veröffentlichung zurückziehen
|
Veröffentlichung zurückziehen
|
||||||
</app-button>
|
</app-button>
|
||||||
}
|
}
|
||||||
@@ -112,6 +118,11 @@
|
|||||||
Teilen
|
Teilen
|
||||||
</app-button>
|
</app-button>
|
||||||
}
|
}
|
||||||
|
@if (show.published && show.reportedType === 'pending') {
|
||||||
|
<app-button (click)="onReport(show)" [icon]="faReport">
|
||||||
|
Melden
|
||||||
|
</app-button>
|
||||||
|
}
|
||||||
@if (!show.published) {
|
@if (!show.published) {
|
||||||
<app-button (click)="onChange(show.id)" [icon]="faSliders">
|
<app-button (click)="onChange(show.id)" [icon]="faSliders">
|
||||||
Ändern
|
Ändern
|
||||||
|
|||||||
@@ -13,6 +13,13 @@
|
|||||||
min-height: calc(100vh - 60px);
|
min-height: calc(100vh - 60px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.show-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.head {
|
.head {
|
||||||
display: flex;
|
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 {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', () => {
|
describe('ShowComponent', () => {
|
||||||
let component: ShowComponent;
|
let component: ShowComponent;
|
||||||
let fixture: ComponentFixture<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(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({
|
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],
|
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();
|
}).compileComponents();
|
||||||
}));
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
fixture = TestBed.createComponent(ShowComponent);
|
fixture = TestBed.createComponent(ShowComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create', () => {
|
it('should create', () => {
|
||||||
void expect(component).toBeTruthy();
|
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,
|
faArrowUpRightFromSquare,
|
||||||
faBox,
|
faBox,
|
||||||
faBoxOpen,
|
faBoxOpen,
|
||||||
|
faCheck,
|
||||||
faChevronRight,
|
faChevronRight,
|
||||||
faFileDownload,
|
faFileDownload,
|
||||||
faLock,
|
faLock,
|
||||||
@@ -50,6 +51,10 @@ import {ButtonComponent} from '../../../widget-modules/components/button/button.
|
|||||||
import {MatMenu, MatMenuTrigger} from '@angular/material/menu';
|
import {MatMenu, MatMenuTrigger} from '@angular/material/menu';
|
||||||
import {ShowTypePipe} from '../../../widget-modules/pipes/show-type-translater/show-type.pipe';
|
import {ShowTypePipe} from '../../../widget-modules/pipes/show-type-translater/show-type.pipe';
|
||||||
import {UserService} from '../../../services/user/user.service';
|
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({
|
@Component({
|
||||||
selector: 'app-show',
|
selector: 'app-show',
|
||||||
@@ -79,6 +84,9 @@ import {UserService} from '../../../services/user/user.service';
|
|||||||
AsyncPipe,
|
AsyncPipe,
|
||||||
DatePipe,
|
DatePipe,
|
||||||
ShowTypePipe,
|
ShowTypePipe,
|
||||||
|
ReportedTypePipe,
|
||||||
|
PublishedTypePipe,
|
||||||
|
BadgeComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class ShowComponent implements OnInit, OnDestroy {
|
export class ShowComponent implements OnInit, OnDestroy {
|
||||||
@@ -101,6 +109,7 @@ export class ShowComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
public faBox = faBox;
|
public faBox = faBox;
|
||||||
public faBoxOpen = faBoxOpen;
|
public faBoxOpen = faBoxOpen;
|
||||||
|
public faReport = faCheck;
|
||||||
public faPublish = faUnlock;
|
public faPublish = faUnlock;
|
||||||
public faUnpublish = faLock;
|
public faUnpublish = faLock;
|
||||||
public faShare = faArrowUpRightFromSquare;
|
public faShare = faArrowUpRightFromSquare;
|
||||||
@@ -185,8 +194,24 @@ export class ShowComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async onPublish(published: boolean): Promise<void> {
|
public async onPublish(show: Show, published: boolean): Promise<void> {
|
||||||
if (this.showId != null) await this.showService.update$(this.showId, {published});
|
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> => {
|
public onShare = async (show: Show): Promise<void> => {
|
||||||
@@ -194,16 +219,69 @@ export class ShowComponent implements OnInit, OnDestroy {
|
|||||||
this.dialog.open(ShareDialogComponent, {data: {url, show}});
|
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 {
|
public getStatus(show: Show): string {
|
||||||
if (show.published) {
|
if (show.published) {
|
||||||
return 'veröffentlicht';
|
return 'veröffentlicht';
|
||||||
}
|
}
|
||||||
if (show.reported) {
|
if (show.reportedType === 'reported') {
|
||||||
return 'gemeldet';
|
return 'gemeldet';
|
||||||
}
|
}
|
||||||
return 'entwurf';
|
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> {
|
public async onDownload(): Promise<void> {
|
||||||
if (this.showId != null) await this.docxService.create(this.showId);
|
if (this.showId != null) await this.docxService.create(this.showId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<span [class]="'badge ' + type">
|
||||||
|
<ng-content></ng-content>
|
||||||
|
</span>
|
||||||
35
src/app/widget-modules/components/badge/badge.component.less
Normal file
35
src/app/widget-modules/components/badge/badge.component.less
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
.badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 1.75rem;
|
||||||
|
padding: 0 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
|
||||||
|
&.ok {
|
||||||
|
background: #e6f6ea;
|
||||||
|
border-color: #9ad1a7;
|
||||||
|
color: #1f6b34;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.warn {
|
||||||
|
background: #fff4db;
|
||||||
|
border-color: #f2cf7a;
|
||||||
|
color: #8a5a00;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
background: #fdeaea;
|
||||||
|
border-color: #efb2b2;
|
||||||
|
color: #9b2c2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.none {
|
||||||
|
background: #eef1f4;
|
||||||
|
border-color: #c7d0d9;
|
||||||
|
color: #4f5f6f;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
import {BadgeComponent} from './badge.component';
|
||||||
|
|
||||||
|
describe('BadgeComponent', () => {
|
||||||
|
let component: BadgeComponent;
|
||||||
|
let fixture: ComponentFixture<BadgeComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [BadgeComponent],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(BadgeComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('create an instance', () => {
|
||||||
|
void expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should apply the configured type class', () => {
|
||||||
|
component.type = 'error';
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(fixture.nativeElement.querySelector('.badge')?.className).toContain('error');
|
||||||
|
});
|
||||||
|
});
|
||||||
13
src/app/widget-modules/components/badge/badge.component.ts
Normal file
13
src/app/widget-modules/components/badge/badge.component.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import {Component, Input} from '@angular/core';
|
||||||
|
|
||||||
|
export type BadgeType = 'ok' | 'warn' | 'error' | 'none';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-badge',
|
||||||
|
templateUrl: './badge.component.html',
|
||||||
|
styleUrls: ['./badge.component.less'],
|
||||||
|
standalone: true,
|
||||||
|
})
|
||||||
|
export class BadgeComponent {
|
||||||
|
@Input() public type: BadgeType = 'none';
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import {PublishedTypePipe} from './published-type.pipe';
|
||||||
|
|
||||||
|
describe('PublishedTypePipe', () => {
|
||||||
|
it('create an instance', () => {
|
||||||
|
const pipe = new PublishedTypePipe();
|
||||||
|
void expect(pipe).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should translate publication state', () => {
|
||||||
|
const pipe = new PublishedTypePipe();
|
||||||
|
|
||||||
|
expect(pipe.transform(true)).toBe('veröffentlicht');
|
||||||
|
expect(pipe.transform(false)).toBe('unveröffentlicht');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import {Pipe, PipeTransform} from '@angular/core';
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'publishedType',
|
||||||
|
})
|
||||||
|
export class PublishedTypePipe implements PipeTransform {
|
||||||
|
public transform(published: boolean): string {
|
||||||
|
return published ? 'veröffentlicht' : 'unveröffentlicht';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import {ReportedTypePipe} from './reported-type.pipe';
|
||||||
|
|
||||||
|
describe('ReportedTypePipe', () => {
|
||||||
|
it('create an instance', () => {
|
||||||
|
const pipe = new ReportedTypePipe();
|
||||||
|
void expect(pipe).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should translate report states', () => {
|
||||||
|
const pipe = new ReportedTypePipe();
|
||||||
|
|
||||||
|
expect(pipe.transform('pending')).toBe('nicht gemeldet');
|
||||||
|
expect(pipe.transform('reported')).toBe('gemeldet');
|
||||||
|
expect(pipe.transform('not-required')).toBe('Meldung nicht notwendig');
|
||||||
|
expect(pipe.transform(null)).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import {Pipe, PipeTransform} from '@angular/core';
|
||||||
|
import {ReportedType} from '../../../modules/shows/services/show';
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'reportedType',
|
||||||
|
})
|
||||||
|
export class ReportedTypePipe implements PipeTransform {
|
||||||
|
public transform(reportedType: ReportedType): string {
|
||||||
|
switch (reportedType) {
|
||||||
|
case 'pending':
|
||||||
|
return 'nicht gemeldet';
|
||||||
|
case 'reported':
|
||||||
|
return 'gemeldet';
|
||||||
|
case 'not-required':
|
||||||
|
return 'Meldung nicht notwendig';
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user