Compare commits
3 Commits
f9516bbc4d
...
ecb25ee322
| Author | SHA1 | Date | |
|---|---|---|---|
| ecb25ee322 | |||
| 2173ad6abf | |||
| 3bd359ee9e |
19
angular.json
19
angular.json
@@ -51,7 +51,7 @@
|
|||||||
"docx",
|
"docx",
|
||||||
"qrcode"
|
"qrcode"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
"fileReplacements": [
|
"fileReplacements": [
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
{
|
{
|
||||||
"type": "initial",
|
"type": "initial",
|
||||||
"maximumWarning": "500kB",
|
"maximumWarning": "500kB",
|
||||||
"maximumError": "1MB"
|
"maximumError": "10MB"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "anyComponentStyle",
|
"type": "anyComponentStyle",
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
},
|
},
|
||||||
"defaultConfiguration": "production"
|
"defaultConfiguration": "production"
|
||||||
},
|
},
|
||||||
"serve": {
|
"serve": {
|
||||||
"builder": "@angular/build:dev-server",
|
"builder": "@angular/build:dev-server",
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
@@ -95,9 +95,20 @@
|
|||||||
"defaultConfiguration": "development"
|
"defaultConfiguration": "development"
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"builder": "@angular/build:unit-test"
|
"builder": "@angular/build:unit-test",
|
||||||
|
"options": {
|
||||||
|
"runner": "vitest",
|
||||||
|
"tsConfig": "tsconfig.spec.json",
|
||||||
|
"setupFiles": [
|
||||||
|
"src/test-vitest.ts"
|
||||||
|
],
|
||||||
|
"runnerConfig": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"cli": {
|
||||||
|
"analytics": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
// Karma configuration file, see link for more information
|
|
||||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
|
||||||
|
|
||||||
module.exports = function (config) {
|
|
||||||
config.set({
|
|
||||||
basePath: '',
|
|
||||||
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
|
||||||
plugins: [
|
|
||||||
require('karma-jasmine'),
|
|
||||||
require('karma-chrome-launcher'),
|
|
||||||
require('karma-jasmine-html-reporter'),
|
|
||||||
require('karma-coverage-istanbul-reporter'),
|
|
||||||
require('@angular-devkit/build-angular/plugins/karma')
|
|
||||||
],
|
|
||||||
client: {
|
|
||||||
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
|
||||||
},
|
|
||||||
coverageIstanbulReporter: {
|
|
||||||
dir: require('path').join(__dirname, './coverage/wgenerator'),
|
|
||||||
reports: ['html', 'lcovonly', 'text-summary'],
|
|
||||||
fixWebpackSourcePaths: true
|
|
||||||
},
|
|
||||||
reporters: ['progress', 'kjhtml'],
|
|
||||||
port: 9876,
|
|
||||||
colors: true,
|
|
||||||
logLevel: config.LOG_INFO,
|
|
||||||
autoWatch: true,
|
|
||||||
browsers: ['Chrome'],
|
|
||||||
singleRun: false,
|
|
||||||
restartOnFileChange: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
3527
package-lock.json
generated
3527
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
@@ -47,22 +47,15 @@
|
|||||||
"@angular/cli": "^21.2.1",
|
"@angular/cli": "^21.2.1",
|
||||||
"@angular/compiler-cli": "^21.2.2",
|
"@angular/compiler-cli": "^21.2.2",
|
||||||
"@angular/language-service": "^21.2.2",
|
"@angular/language-service": "^21.2.2",
|
||||||
"@types/jasmine": "~6.0.0",
|
|
||||||
"@types/jasminewd2": "~2.0.13",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^8.57.0",
|
"@typescript-eslint/eslint-plugin": "^8.57.0",
|
||||||
"@typescript-eslint/parser": "^8.57.0",
|
"@typescript-eslint/parser": "^8.57.0",
|
||||||
"eslint": "^9.39.4",
|
"eslint": "^9.39.4",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-prettier": "^5.5.5",
|
"eslint-plugin-prettier": "^5.5.5",
|
||||||
"firebase-tools": "^15.9.1",
|
"firebase-tools": "^15.9.1",
|
||||||
"jasmine-core": "~6.1.0",
|
"jsdom": "^29.0.0",
|
||||||
"jasmine-spec-reporter": "~7.0.0",
|
|
||||||
"karma": "~6.4.4",
|
|
||||||
"karma-chrome-launcher": "~3.2.0",
|
|
||||||
"karma-coverage-istanbul-reporter": "~3.0.3",
|
|
||||||
"karma-jasmine": "~5.1.0",
|
|
||||||
"karma-jasmine-html-reporter": "^2.2.0",
|
|
||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
"typescript": "~5.9.3"
|
"typescript": "~5.9.3",
|
||||||
|
"vitest": "^4.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<app-navigation></app-navigation>
|
<app-navigation></app-navigation>
|
||||||
|
|
||||||
<div [@fader]="o.isActivated ? o.activatedRoute : ''" class="content">
|
<div class="content">
|
||||||
<router-outlet #o="outlet"></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import {TestBed, waitForAsync} from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
import {RouterTestingModule} from '@angular/router/testing';
|
import {RouterTestingModule} from '@angular/router/testing';
|
||||||
import {AppComponent} from './app.component';
|
import {AppComponent} from './app.component';
|
||||||
|
|
||||||
describe('AppComponent', () => {
|
describe('AppComponent', () => {
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [RouterTestingModule, AppComponent],
|
imports: [RouterTestingModule, AppComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
});
|
||||||
|
|
||||||
it('should create the app', () => {
|
it('should create the app', () => {
|
||||||
const fixture = TestBed.createComponent(AppComponent);
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {BrandComponent} from './brand.component';
|
import {BrandComponent} from './brand.component';
|
||||||
|
|
||||||
@@ -6,11 +6,11 @@ describe('BrandComponent', () => {
|
|||||||
let component: BrandComponent;
|
let component: BrandComponent;
|
||||||
let fixture: ComponentFixture<BrandComponent>;
|
let fixture: ComponentFixture<BrandComponent>;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [BrandComponent],
|
imports: [BrandComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(BrandComponent);
|
fixture = TestBed.createComponent(BrandComponent);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {NewUserComponent} from './new-user.component';
|
import {NewUserComponent} from './new-user.component';
|
||||||
|
|
||||||
@@ -6,11 +6,11 @@ describe('NewUserComponent', () => {
|
|||||||
let component: NewUserComponent;
|
let component: NewUserComponent;
|
||||||
let fixture: ComponentFixture<NewUserComponent>;
|
let fixture: ComponentFixture<NewUserComponent>;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [NewUserComponent],
|
imports: [NewUserComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(NewUserComponent);
|
fixture = TestBed.createComponent(NewUserComponent);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {GuestComponent} from './guest.component';
|
import {GuestComponent} from './guest.component';
|
||||||
|
|
||||||
@@ -6,11 +6,11 @@ describe('GuestComponent', () => {
|
|||||||
let component: GuestComponent;
|
let component: GuestComponent;
|
||||||
let fixture: ComponentFixture<GuestComponent>;
|
let fixture: ComponentFixture<GuestComponent>;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [GuestComponent],
|
imports: [GuestComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(GuestComponent);
|
fixture = TestBed.createComponent(GuestComponent);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {LegalComponent} from './legal.component';
|
import {LegalComponent} from './legal.component';
|
||||||
|
|
||||||
@@ -6,11 +6,11 @@ describe('LegalComponent', () => {
|
|||||||
let component: LegalComponent;
|
let component: LegalComponent;
|
||||||
let fixture: ComponentFixture<LegalComponent>;
|
let fixture: ComponentFixture<LegalComponent>;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [LegalComponent],
|
imports: [LegalComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(LegalComponent);
|
fixture = TestBed.createComponent(LegalComponent);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {LogoComponent} from './logo.component';
|
import {LogoComponent} from './logo.component';
|
||||||
|
|
||||||
@@ -6,11 +6,11 @@ describe('LogoComponent', () => {
|
|||||||
let component: LogoComponent;
|
let component: LogoComponent;
|
||||||
let fixture: ComponentFixture<LogoComponent>;
|
let fixture: ComponentFixture<LogoComponent>;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [LogoComponent],
|
imports: [LogoComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(LogoComponent);
|
fixture = TestBed.createComponent(LogoComponent);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {MonitorComponent} from './monitor.component';
|
import {MonitorComponent} from './monitor.component';
|
||||||
|
|
||||||
@@ -6,11 +6,11 @@ describe('MonitorComponent', () => {
|
|||||||
let component: MonitorComponent;
|
let component: MonitorComponent;
|
||||||
let fixture: ComponentFixture<MonitorComponent>;
|
let fixture: ComponentFixture<MonitorComponent>;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [MonitorComponent],
|
imports: [MonitorComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(MonitorComponent);
|
fixture = TestBed.createComponent(MonitorComponent);
|
||||||
|
|||||||
@@ -1,83 +1,81 @@
|
|||||||
@if (show) {
|
@if (show) {
|
||||||
<div @fade>
|
<app-card [closeIcon]="faIcon" [heading]="show.showType | showType" [subheading]="show.date.toDate() | date:'dd.MM.yyyy'" closeLink="/presentation/select">
|
||||||
<app-card [closeIcon]="faIcon" [heading]="show.showType | showType" [subheading]="show.date.toDate() | date:'dd.MM.yyyy'" closeLink="/presentation/select">
|
@if (!progress) {
|
||||||
@if (!progress) {
|
<div class="song">
|
||||||
<div class="song">
|
@if (show) {
|
||||||
@if (show) {
|
<div class="song-parts">
|
||||||
<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)" [class.active]="show.presentationSongId === 'title'" class="song-part">
|
<div class="head">Veranstaltung</div>
|
||||||
<div class="head">Veranstaltung</div>
|
</div>
|
||||||
</div>
|
<div (click)="onSectionClick('empty', -1, show.id)" [class.active]="show.presentationSongId === 'empty'" class="song-part">
|
||||||
<div (click)="onSectionClick('empty', -1, show.id)" [class.active]="show.presentationSongId === 'empty'" class="song-part">
|
<div class="head">Leer</div>
|
||||||
<div class="head">Leer</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
@for (song of presentationSongs; track trackBy($index, song)) {
|
}
|
||||||
<div class="song">
|
</div>
|
||||||
@if (show) {
|
@for (song of presentationSongs; track trackBy($index, song)) {
|
||||||
<div [class.active]="show.presentationSongId === song.id" class="title song-part">
|
<div class="song">
|
||||||
<div (click)="onSectionClick(song.id, -1, show.id)" class="head">{{ song.title }}</div>
|
@if (show) {
|
||||||
</div>
|
<div [class.active]="show.presentationSongId === song.id" class="title song-part">
|
||||||
} @if (show) {
|
<div (click)="onSectionClick(song.id, -1, show.id)" class="head">{{ song.title }}</div>
|
||||||
<div class="song-parts">
|
</div>
|
||||||
@for (section of song.sections; track section.type + '-' + section.number + '-' + $index; let i = $index) {
|
} @if (show) {
|
||||||
<div
|
<div class="song-parts">
|
||||||
(click)="onSectionClick(song.id, i, show.id)"
|
@for (section of song.sections; track section.type + '-' + section.number + '-' + $index; let i = $index) {
|
||||||
[class.active]="
|
<div
|
||||||
|
(click)="onSectionClick(song.id, i, show.id)"
|
||||||
|
[class.active]="
|
||||||
show.presentationSongId === song.id &&
|
show.presentationSongId === song.id &&
|
||||||
show.presentationSection === i
|
show.presentationSection === i
|
||||||
"
|
"
|
||||||
class="song-part"
|
class="song-part"
|
||||||
>
|
>
|
||||||
<div class="head">{{ section.type | sectionType }} {{ section.number + 1 }}</div>
|
<div class="head">{{ section.type | sectionType }} {{ section.number + 1 }}</div>
|
||||||
<div class="fragment">{{ getFirstLine(section) }}</div>
|
<div class="fragment">{{ getFirstLine(section) }}</div>
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<div class="song">
|
</div>
|
||||||
@if (show) {
|
}
|
||||||
<div [class.active]="show.presentationSongId === 'dynamicText'" class="title song-part">
|
<div class="song">
|
||||||
<div (click)="onSectionClick('dynamicText', -1, show.id)" class="head">Freier Text</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<mat-form-field appearance="outline">
|
|
||||||
<mat-label>Überschrift</mat-label>
|
|
||||||
<input (ngModelChange)="onDynamicCaption($event, show.id)" [ngModel]="show.presentationDynamicCaption" autocomplete="off" id="dynamic-caption" matInput type="text" />
|
|
||||||
</mat-form-field>
|
|
||||||
<mat-form-field appearance="outline">
|
|
||||||
<mat-label>Text</mat-label>
|
|
||||||
<textarea (ngModelChange)="onDynamicText($event, show.id)" [ngModel]="show.presentationDynamicText" autocomplete="off" id="dynamic-text" matInput></textarea>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
@if (show) {
|
@if (show) {
|
||||||
<div class="div-bottom">
|
<div [class.active]="show.presentationSongId === 'dynamicText'" class="title song-part">
|
||||||
<button class="btn-start-presentation" mat-button routerLink="/presentation/monitor">
|
<div (click)="onSectionClick('dynamicText', -1, show.id)" class="head">Freier Text</div>
|
||||||
<fa-icon [icon]="faDesktop"></fa-icon>
|
|
||||||
Präsentation starten
|
|
||||||
</button>
|
|
||||||
<mat-form-field appearance="outline">
|
|
||||||
<mat-label>Hintergrund</mat-label>
|
|
||||||
<mat-select (ngModelChange)="onBackground($event, show.id)" [ngModel]="show.presentationBackground">
|
|
||||||
<mat-option value="none">kein Hintergrund</mat-option>
|
|
||||||
<mat-option value="blue">Sternenhimmel</mat-option>
|
|
||||||
<mat-option value="green">Blätter</mat-option>
|
|
||||||
<mat-option value="leder">Leder</mat-option>
|
|
||||||
<mat-option value="praise">Lobpreis</mat-option>
|
|
||||||
<mat-option value="bible">Bibel</mat-option>
|
|
||||||
</mat-select>
|
|
||||||
</mat-form-field>
|
|
||||||
<mat-slider #slider [max]="100" [min]="10" [step]="2" class="zoom-slider" color="primary" ngDefaultControl
|
|
||||||
><input (ngModelChange)="onZoom($event, show.id)" [ngModel]="show.presentationZoom" matSliderThumb />
|
|
||||||
</mat-slider>
|
|
||||||
</div>
|
</div>
|
||||||
} @if (show) {
|
}
|
||||||
<app-add-song [addedLive]="true" [showSongs]="showSongs" [show]="show" [songs]="songs$|async"></app-add-song>
|
<mat-form-field appearance="outline">
|
||||||
} }
|
<mat-label>Überschrift</mat-label>
|
||||||
</app-card>
|
<input (ngModelChange)="onDynamicCaption($event, show.id)" [ngModel]="show.presentationDynamicCaption" autocomplete="off" id="dynamic-caption" matInput type="text" />
|
||||||
</div>
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Text</mat-label>
|
||||||
|
<textarea (ngModelChange)="onDynamicText($event, show.id)" [ngModel]="show.presentationDynamicText" autocomplete="off" id="dynamic-text" matInput></textarea>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
@if (show) {
|
||||||
|
<div class="div-bottom">
|
||||||
|
<button class="btn-start-presentation" mat-button routerLink="/presentation/monitor">
|
||||||
|
<fa-icon [icon]="faDesktop"></fa-icon>
|
||||||
|
Präsentation starten
|
||||||
|
</button>
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Hintergrund</mat-label>
|
||||||
|
<mat-select (ngModelChange)="onBackground($event, show.id)" [ngModel]="show.presentationBackground">
|
||||||
|
<mat-option value="none">kein Hintergrund</mat-option>
|
||||||
|
<mat-option value="blue">Sternenhimmel</mat-option>
|
||||||
|
<mat-option value="green">Blätter</mat-option>
|
||||||
|
<mat-option value="leder">Leder</mat-option>
|
||||||
|
<mat-option value="praise">Lobpreis</mat-option>
|
||||||
|
<mat-option value="bible">Bibel</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-slider #slider [max]="100" [min]="10" [step]="2" class="zoom-slider" color="primary" ngDefaultControl
|
||||||
|
><input (ngModelChange)="onZoom($event, show.id)" [ngModel]="show.presentationZoom" matSliderThumb />
|
||||||
|
</mat-slider>
|
||||||
|
</div>
|
||||||
|
} @if (show) {
|
||||||
|
<app-add-song [addedLive]="true" [showSongs]="showSongs" [show]="show" [songs]="songs$|async"></app-add-song>
|
||||||
|
} }
|
||||||
|
</app-card>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {RemoteComponent} from './remote.component';
|
import {RemoteComponent} from './remote.component';
|
||||||
|
|
||||||
@@ -6,11 +6,11 @@ describe('RemoteComponent', () => {
|
|||||||
let component: RemoteComponent;
|
let component: RemoteComponent;
|
||||||
let fixture: ComponentFixture<RemoteComponent>;
|
let fixture: ComponentFixture<RemoteComponent>;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [RemoteComponent],
|
imports: [RemoteComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(RemoteComponent);
|
fixture = TestBed.createComponent(RemoteComponent);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
import {Router} from '@angular/router';
|
import {Router} from '@angular/router';
|
||||||
import {of} from 'rxjs';
|
import {firstValueFrom, of} from 'rxjs';
|
||||||
import {GlobalSettingsService} from '../../../services/global-settings.service';
|
import {GlobalSettingsService} from '../../../services/global-settings.service';
|
||||||
import {ShowService} from '../../shows/services/show.service';
|
import {ShowService} from '../../shows/services/show.service';
|
||||||
import {SelectComponent} from './select.component';
|
import {SelectComponent} from './select.component';
|
||||||
@@ -52,12 +52,11 @@ describe('SelectComponent', () => {
|
|||||||
expect(component.visible).toBeTrue();
|
expect(component.visible).toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should expose recent shows sorted descending by date', done => {
|
it('should expose recent shows sorted descending by date', async () => {
|
||||||
component.shows$.subscribe(shows => {
|
const shows = await firstValueFrom(component.shows$);
|
||||||
expect(showServiceSpy.list$).toHaveBeenCalledWith(true);
|
|
||||||
expect(shows.map(show => show.id)).toEqual(['recent-a', 'recent-b']);
|
expect(showServiceSpy.list$).toHaveBeenCalledWith(true);
|
||||||
done();
|
expect(shows.map(show => show.id)).toEqual(['recent-a', 'recent-b']);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should persist the selected show, trigger presentation reset and navigate', async () => {
|
it('should persist the selected show, trigger presentation reset and navigate', async () => {
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import {PresentationService} from './presentation.service';
|
|||||||
describe('PresentationService', () => {
|
describe('PresentationService', () => {
|
||||||
let service: PresentationService;
|
let service: PresentationService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({});
|
await TestBed.configureTestingModule({});
|
||||||
service = TestBed.inject(PresentationService);
|
service = TestBed.inject(PresentationService);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,4 +2,5 @@ export interface FilterValues {
|
|||||||
time: number;
|
time: number;
|
||||||
owner: string;
|
owner: string;
|
||||||
showType: string;
|
showType: string;
|
||||||
|
archived: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<mat-label>Zeitraum</mat-label>
|
<mat-label>Zeitraum</mat-label>
|
||||||
<mat-select formControlName="time">
|
<mat-select formControlName="time">
|
||||||
@for (time of times; track time) {
|
@for (time of times; track time) {
|
||||||
<mat-option [value]="time.key">{{ time.value }} </mat-option>
|
<mat-option [value]="time.key">{{ time.value }}</mat-option>
|
||||||
}
|
}
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
<mat-select formControlName="owner">
|
<mat-select formControlName="owner">
|
||||||
<mat-option value="">Alle</mat-option>
|
<mat-option value="">Alle</mat-option>
|
||||||
@for (owner of owners; track owner) {
|
@for (owner of owners; track owner) {
|
||||||
<mat-option [value]="owner.key">{{ owner.value }} </mat-option>
|
<mat-option [value]="owner.key">{{ owner.value }}</mat-option>
|
||||||
}
|
}
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
@@ -25,17 +25,18 @@
|
|||||||
<mat-option value="">Alle</mat-option>
|
<mat-option value="">Alle</mat-option>
|
||||||
<mat-optgroup label="öffentlich">
|
<mat-optgroup label="öffentlich">
|
||||||
@for (key of showTypePublic; track key) {
|
@for (key of showTypePublic; track key) {
|
||||||
<mat-option [value]="key">{{ key | showType }} </mat-option>
|
<mat-option [value]="key">{{ key | showType }}</mat-option>
|
||||||
}
|
}
|
||||||
</mat-optgroup>
|
</mat-optgroup>
|
||||||
<mat-optgroup label="privat">
|
<mat-optgroup label="privat">
|
||||||
@for (key of showTypePrivate; track key) {
|
@for (key of showTypePrivate; track key) {
|
||||||
<mat-option [value]="key">{{ key | showType }} </mat-option>
|
<mat-option [value]="key">{{ key | showType }}</mat-option>
|
||||||
}
|
}
|
||||||
</mat-optgroup>
|
</mat-optgroup>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
<mat-checkbox formControlName="archived">Archiviert</mat-checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<i>Anzahl der Suchergebnisse: {{ shows?.length ?? 0 }}</i>
|
<!-- <i>Anzahl der Suchergebnisse: {{ shows?.length ?? 0 }}</i>-->
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
.third {
|
.third,
|
||||||
display: grid;
|
div[formGroup] {
|
||||||
grid-template-columns: 1fr 1fr 1fr;
|
display: flex;
|
||||||
column-gap: 20px;
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.third {
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .mat-mdc-form-field {
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {Component, DestroyRef, Input, inject} from '@angular/core';
|
import {Component, DestroyRef, inject, Input} from '@angular/core';
|
||||||
import {KeyValue} from '@angular/common';
|
import {KeyValue} from '@angular/common';
|
||||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||||
import {FormBuilder, FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms';
|
import {FormBuilder, FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms';
|
||||||
@@ -14,28 +14,23 @@ import {MatFormField, MatLabel} from '@angular/material/form-field';
|
|||||||
import {MatSelect} from '@angular/material/select';
|
import {MatSelect} from '@angular/material/select';
|
||||||
import {MatOptgroup, MatOption} from '@angular/material/core';
|
import {MatOptgroup, MatOption} from '@angular/material/core';
|
||||||
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 {MatCheckbox} from '@angular/material/checkbox';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-filter',
|
selector: 'app-filter',
|
||||||
templateUrl: './filter.component.html',
|
templateUrl: './filter.component.html',
|
||||||
styleUrls: ['./filter.component.less'],
|
styleUrls: ['./filter.component.less'],
|
||||||
imports: [ReactiveFormsModule, MatFormField, MatLabel, MatSelect, MatOption, MatOptgroup, ShowTypePipe],
|
imports: [ReactiveFormsModule, MatFormField, MatLabel, MatSelect, MatOption, MatOptgroup, ShowTypePipe, MatCheckbox],
|
||||||
})
|
})
|
||||||
export class FilterComponent {
|
export class FilterComponent {
|
||||||
private showService = inject(ShowService);
|
|
||||||
private userService = inject(UserService);
|
|
||||||
private filterStore = inject(FilterStoreService);
|
|
||||||
private destroyRef = inject(DestroyRef);
|
|
||||||
|
|
||||||
@Input() public shows: Show[] = [];
|
@Input() public shows: Show[] = [];
|
||||||
|
|
||||||
public showTypePublic = ShowService.SHOW_TYPE_PUBLIC;
|
public showTypePublic = ShowService.SHOW_TYPE_PUBLIC;
|
||||||
public showTypePrivate = ShowService.SHOW_TYPE_PRIVATE;
|
public showTypePrivate = ShowService.SHOW_TYPE_PRIVATE;
|
||||||
|
|
||||||
public filterFormGroup: FormGroup<{
|
public filterFormGroup: FormGroup<{
|
||||||
time: FormControl<number>;
|
time: FormControl<number>;
|
||||||
owner: FormControl<string | null>;
|
owner: FormControl<string | null>;
|
||||||
showType: FormControl<string | null>;
|
showType: FormControl<string | null>;
|
||||||
|
archived: FormControl<boolean>;
|
||||||
}>;
|
}>;
|
||||||
public times: KeyValue<number, string>[] = [
|
public times: KeyValue<number, string>[] = [
|
||||||
{key: 1, value: 'letzter Monat'},
|
{key: 1, value: 'letzter Monat'},
|
||||||
@@ -43,8 +38,11 @@ export class FilterComponent {
|
|||||||
{key: 12, value: 'letztes Jahr'},
|
{key: 12, value: 'letztes Jahr'},
|
||||||
{key: 99999, value: 'alle'},
|
{key: 99999, value: 'alle'},
|
||||||
];
|
];
|
||||||
|
|
||||||
public owners: {key: string; value: string}[] = [];
|
public owners: {key: string; value: string}[] = [];
|
||||||
|
private showService = inject(ShowService);
|
||||||
|
private userService = inject(UserService);
|
||||||
|
private filterStore = inject(FilterStoreService);
|
||||||
|
private destroyRef = inject(DestroyRef);
|
||||||
|
|
||||||
public constructor() {
|
public constructor() {
|
||||||
const fb = inject(FormBuilder);
|
const fb = inject(FormBuilder);
|
||||||
@@ -53,6 +51,7 @@ export class FilterComponent {
|
|||||||
time: fb.nonNullable.control(1),
|
time: fb.nonNullable.control(1),
|
||||||
owner: fb.control<string | null>(null),
|
owner: fb.control<string | null>(null),
|
||||||
showType: fb.control<string | null>(null),
|
showType: fb.control<string | null>(null),
|
||||||
|
archived: fb.nonNullable.control(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
this.filterStore.showFilter$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(filterValues => {
|
this.filterStore.showFilter$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(filterValues => {
|
||||||
@@ -61,6 +60,7 @@ export class FilterComponent {
|
|||||||
time: filterValues.time,
|
time: filterValues.time,
|
||||||
owner: filterValues.owner || null,
|
owner: filterValues.owner || null,
|
||||||
showType: filterValues.showType || null,
|
showType: filterValues.showType || null,
|
||||||
|
archived: !!filterValues.archived,
|
||||||
},
|
},
|
||||||
{emitEvent: false}
|
{emitEvent: false}
|
||||||
);
|
);
|
||||||
@@ -69,6 +69,7 @@ export class FilterComponent {
|
|||||||
this.filterFormGroup.controls.time.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => this.filterValueChanged('time', value));
|
this.filterFormGroup.controls.time.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => this.filterValueChanged('time', value));
|
||||||
this.filterFormGroup.controls.owner.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => this.filterValueChanged('owner', value ?? ''));
|
this.filterFormGroup.controls.owner.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => this.filterValueChanged('owner', value ?? ''));
|
||||||
this.filterFormGroup.controls.showType.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => this.filterValueChanged('showType', value ?? ''));
|
this.filterFormGroup.controls.showType.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => this.filterValueChanged('showType', value ?? ''));
|
||||||
|
this.filterFormGroup.controls.archived.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => this.filterValueChanged('archived', value));
|
||||||
|
|
||||||
this.owners$()
|
this.owners$()
|
||||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||||
|
|||||||
@@ -1,31 +1,42 @@
|
|||||||
<div>
|
@if (showSidebar$ | async) {
|
||||||
<!-- <app-list-header *appRole="['leader']"></app-list-header>-->
|
<app-sidebar>
|
||||||
<app-list-header *appRole="['leader']">
|
<div class="sidebar-content" sidebar>
|
||||||
@if (shows$ | async; as shows) {
|
<app-filter [shows]="(publicShows$ | async) ?? []"></app-filter>
|
||||||
<app-filter [shows]="publicShows$ | async"></app-filter>
|
</div>
|
||||||
}
|
<div content>
|
||||||
</app-list-header>
|
@if (privateShows$ | async; as privateShows) {
|
||||||
|
|
||||||
<ng-container *appRole="['leader']">
|
|
||||||
@if (privateShows$ | async; as shows) { @if (shows.length > 0) {
|
|
||||||
<app-card [padding]="false" heading="Meine Veranstaltungen">
|
<app-card [padding]="false" heading="Meine Veranstaltungen">
|
||||||
@for (show of shows | sortBy: 'desc':'date'; track trackBy($index, show)) {
|
@for (show of privateShows; 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.archived ? 'warn' : show.published ? 'error' : 'none'"
|
||||||
[showStatusBadgeType]="show.published ? 'error' : 'none'"
|
[showStatusBadge]="show.archived ? 'archiviert' : show.published ? 'nicht gemeldet' : 'unveröffentlicht'"
|
||||||
[show]="show"
|
[show]="show"
|
||||||
></app-list-item>
|
></app-list-item>
|
||||||
}
|
}
|
||||||
|
<div *appRole="['leader']" class="list-action">
|
||||||
|
<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) {
|
||||||
|
<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>
|
||||||
|
}
|
||||||
</app-card>
|
</app-card>
|
||||||
} }
|
} }
|
||||||
</ng-container>
|
</div>
|
||||||
|
</app-sidebar>
|
||||||
|
} @else {
|
||||||
|
<div>
|
||||||
@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">
|
<app-card [padding]="false" heading="Veröffentlichte Veranstaltungen">
|
||||||
@for (show of shows | sortBy: 'desc':'date'; track trackBy($index, show)) {
|
@for (show of shows; track trackBy($index, show)) {
|
||||||
<app-list-item [routerLink]="show.id" [show]="show"></app-list-item>
|
<app-list-item [routerLink]="show.id" [show]="show"></app-list-item>
|
||||||
}
|
}
|
||||||
</app-card>
|
</app-card>
|
||||||
} }
|
} }
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.sidebar-content {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
import {BehaviorSubject, of} from 'rxjs';
|
import {BehaviorSubject, firstValueFrom, of} from 'rxjs';
|
||||||
|
import {skip, take} from 'rxjs/operators';
|
||||||
import {ListComponent} from './list.component';
|
import {ListComponent} from './list.component';
|
||||||
import {ShowService} from '../services/show.service';
|
import {ShowService} from '../services/show.service';
|
||||||
import {UserService} from '../../../services/user/user.service';
|
import {UserService} from '../../../services/user/user.service';
|
||||||
@@ -50,7 +51,7 @@ describe('ListComponent', () => {
|
|||||||
void expect(component).toBeTruthy();
|
void expect(component).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should list own drafts and pending published shows in my shows', done => {
|
it('should list own drafts and pending published shows in my shows', async () => {
|
||||||
shows$.next([
|
shows$.next([
|
||||||
createShow({id: 'draft-own', owner: 'user-1', published: false, reportedType: null}),
|
createShow({id: 'draft-own', owner: 'user-1', published: false, reportedType: null}),
|
||||||
createShow({id: 'pending-own', owner: 'user-1', published: true, reportedType: 'pending', date: {toDate: () => new Date('2026-03-02')}}),
|
createShow({id: 'pending-own', owner: 'user-1', published: true, reportedType: 'pending', date: {toDate: () => new Date('2026-03-02')}}),
|
||||||
@@ -58,13 +59,11 @@ describe('ListComponent', () => {
|
|||||||
createShow({id: 'draft-other', owner: 'user-2', published: false, reportedType: null, date: {toDate: () => new Date('2026-03-04')}}),
|
createShow({id: 'draft-other', owner: 'user-2', published: false, reportedType: null, date: {toDate: () => new Date('2026-03-04')}}),
|
||||||
] as never);
|
] as never);
|
||||||
|
|
||||||
component.privateShows$.subscribe(shows => {
|
const shows = await firstValueFrom(component.privateShows$);
|
||||||
expect(shows.map(show => show.id)).toEqual(['draft-own', 'pending-own']);
|
expect(shows.map(show => show.id)).toEqual(['pending-own', 'draft-own']);
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should ignore show filters for my shows', done => {
|
it('should ignore show filters for my shows', async () => {
|
||||||
const filterStore = TestBed.inject(FilterStoreService);
|
const filterStore = TestBed.inject(FilterStoreService);
|
||||||
filterStore.updateShowFilter({time: 0, showType: 'service-worship'});
|
filterStore.updateShowFilter({time: 0, showType: 'service-worship'});
|
||||||
shows$.next([
|
shows$.next([
|
||||||
@@ -72,9 +71,36 @@ describe('ListComponent', () => {
|
|||||||
createShow({id: 'pending-own', owner: 'user-1', published: true, reportedType: 'pending', showType: 'home-group', date: {toDate: () => new Date('2026-03-05')}}),
|
createShow({id: 'pending-own', owner: 'user-1', published: true, reportedType: 'pending', showType: 'home-group', date: {toDate: () => new Date('2026-03-05')}}),
|
||||||
] as never);
|
] as never);
|
||||||
|
|
||||||
component.privateShows$.subscribe(shows => {
|
const shows = await firstValueFrom(component.privateShows$);
|
||||||
expect(shows.map(show => show.id)).toEqual(['older-draft', 'pending-own']);
|
expect(shows.map(show => show.id)).toEqual(['pending-own', 'older-draft']);
|
||||||
done();
|
});
|
||||||
});
|
|
||||||
|
it('should hide archived own shows until archived filter is enabled', async () => {
|
||||||
|
const filterStore = TestBed.inject(FilterStoreService);
|
||||||
|
shows$.next([
|
||||||
|
createShow({id: 'draft-own', owner: 'user-1', published: false, reportedType: null, date: {toDate: () => new Date('2026-03-02')}}),
|
||||||
|
createShow({id: 'archived-own', owner: 'user-1', published: true, archived: true, reportedType: 'reported', date: {toDate: () => new Date('2026-03-03')}}),
|
||||||
|
] as never);
|
||||||
|
|
||||||
|
const initialShows = await firstValueFrom(component.privateShows$.pipe(take(1)));
|
||||||
|
expect(initialShows.map(show => show.id)).toEqual(['draft-own']);
|
||||||
|
|
||||||
|
const updatedShowsPromise = firstValueFrom(component.privateShows$.pipe(skip(1), take(1)));
|
||||||
|
filterStore.updateShowFilter({archived: true});
|
||||||
|
const updatedShows = await updatedShowsPromise;
|
||||||
|
expect(updatedShows.map(show => show.id)).toEqual(['archived-own', 'draft-own']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sort public shows by date descending', async () => {
|
||||||
|
const filterStore = TestBed.inject(FilterStoreService);
|
||||||
|
filterStore.updateShowFilter({time: 99999});
|
||||||
|
shows$.next([
|
||||||
|
createShow({id: 'old-public', owner: 'user-2', published: true, archived: false, date: {toDate: () => new Date('2026-01-01')}}),
|
||||||
|
createShow({id: 'new-public', owner: 'user-3', published: true, archived: false, date: {toDate: () => new Date('2026-03-10')}}),
|
||||||
|
createShow({id: 'mid-public', owner: 'user-4', published: true, archived: false, date: {toDate: () => new Date('2026-02-05')}}),
|
||||||
|
] as never);
|
||||||
|
|
||||||
|
const shows = await firstValueFrom(component.publicShows$.pipe(take(1)));
|
||||||
|
expect(shows.map(show => show.id)).toEqual(['new-public', 'mid-public', 'old-public']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,23 +7,25 @@ import {FilterValues} from './filter/filter-values';
|
|||||||
import {RouterLink} from '@angular/router';
|
import {RouterLink} from '@angular/router';
|
||||||
import {map, switchMap} from 'rxjs/operators';
|
import {map, switchMap} from 'rxjs/operators';
|
||||||
import {FilterStoreService} from '../../../services/filter-store.service';
|
import {FilterStoreService} from '../../../services/filter-store.service';
|
||||||
import {RoleDirective} from '../../../services/user/role.directive';
|
|
||||||
import {ListHeaderComponent} from '../../../widget-modules/components/list-header/list-header.component';
|
|
||||||
import {AsyncPipe} from '@angular/common';
|
import {AsyncPipe} from '@angular/common';
|
||||||
import {FilterComponent} from './filter/filter.component';
|
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 {UserService} from '../../../services/user/user.service';
|
import {UserService} from '../../../services/user/user.service';
|
||||||
|
import {SidebarComponent} from '../../../widget-modules/components/sidebar/sidebar.component';
|
||||||
|
import {ButtonComponent} from '../../../widget-modules/components/button/button.component';
|
||||||
|
import {faPlus} from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import {RoleDirective} from '../../../services/user/role.directive';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-list',
|
selector: 'app-list',
|
||||||
templateUrl: './list.component.html',
|
templateUrl: './list.component.html',
|
||||||
styleUrls: ['./list.component.less'],
|
styleUrls: ['./list.component.less'],
|
||||||
animations: [fade],
|
animations: [fade],
|
||||||
imports: [RoleDirective, ListHeaderComponent, FilterComponent, CardComponent, ListItemComponent, RouterLink, AsyncPipe, SortByPipe],
|
imports: [FilterComponent, CardComponent, ListItemComponent, RouterLink, AsyncPipe, SidebarComponent, ButtonComponent, RoleDirective],
|
||||||
})
|
})
|
||||||
export class ListComponent {
|
export class ListComponent {
|
||||||
|
public faNewShow = faPlus;
|
||||||
private showService = inject(ShowService);
|
private showService = inject(ShowService);
|
||||||
private filterStore = inject(FilterStoreService);
|
private filterStore = inject(FilterStoreService);
|
||||||
private userService = inject(UserService);
|
private userService = inject(UserService);
|
||||||
@@ -32,9 +34,24 @@ export class ListComponent {
|
|||||||
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 archived$ = this.filter$.pipe(map((filterValues: FilterValues) => !!filterValues.archived));
|
||||||
public shows$ = this.showService.list$();
|
public shows$ = this.showService.list$();
|
||||||
public privateShows$ = combineLatest([this.shows$, this.userService.user$]).pipe(
|
public ownShows$ = this.showService.list$(false, true);
|
||||||
map(([shows, user]) => shows.filter(show => show.owner === user?.id).filter(show => !show.published || show.reportedType === 'pending'))
|
public privateShows$ = combineLatest([this.ownShows$, this.userService.user$, this.archived$]).pipe(
|
||||||
|
map(([shows, user, showArchived]) =>
|
||||||
|
shows.filter(show => {
|
||||||
|
if (show.owner !== user?.id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (show.archived) {
|
||||||
|
return showArchived;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !show.published || show.reportedType === 'pending';
|
||||||
|
})
|
||||||
|
),
|
||||||
|
map(shows => this.sortShowsByDateDesc(shows))
|
||||||
);
|
);
|
||||||
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(
|
||||||
@@ -46,9 +63,10 @@ export class ListComponent {
|
|||||||
map(([queriedShows, fallbackShows, owner, showType]) => {
|
map(([queriedShows, fallbackShows, owner, showType]) => {
|
||||||
const shows = queriedShows.length > 0 || fallbackShows.length === 0 ? queriedShows : fallbackShows;
|
const shows = queriedShows.length > 0 || fallbackShows.length === 0 ? queriedShows : fallbackShows;
|
||||||
|
|
||||||
return shows.filter(show => !owner || show.owner === owner).filter(show => !showType || show.showType === showType);
|
return this.sortShowsByDateDesc(shows.filter(show => !owner || show.owner === owner).filter(show => !showType || show.showType === showType));
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
public showSidebar$ = this.userService.user$.pipe(map(user => this.hasSidebarAccess(user?.role)));
|
||||||
|
|
||||||
public trackBy = (index: number, show: unknown) => (show as Show).id;
|
public trackBy = (index: number, show: unknown) => (show as Show).id;
|
||||||
|
|
||||||
@@ -58,4 +76,17 @@ export class ListComponent {
|
|||||||
startDate.setDate(startDate.getDate() - lastMonths * 30);
|
startDate.setDate(startDate.getDate() - lastMonths * 30);
|
||||||
return show.date.toDate() >= startDate;
|
return show.date.toDate() >= startDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sortShowsByDateDesc(shows: Show[]): Show[] {
|
||||||
|
return [...shows].sort((left, right) => right.date.toDate().getTime() - left.date.toDate().getTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
private hasSidebarAccess(role: string | null | undefined): boolean {
|
||||||
|
if (!role) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const roles = role.split(';').map(item => item.trim());
|
||||||
|
return roles.includes('admin') || roles.includes('leader');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {NewComponent} from './new.component';
|
import {NewComponent} from './new.component';
|
||||||
|
|
||||||
@@ -6,11 +6,11 @@ describe('NewComponent', () => {
|
|||||||
let component: NewComponent;
|
let component: NewComponent;
|
||||||
let fixture: ComponentFixture<NewComponent>;
|
let fixture: ComponentFixture<NewComponent>;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [NewComponent],
|
imports: [NewComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(NewComponent);
|
fixture = TestBed.createComponent(NewComponent);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {TestBed} from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
import {BehaviorSubject, of} from 'rxjs';
|
import {BehaviorSubject, firstValueFrom, of} from 'rxjs';
|
||||||
import {ShowDataService} from './show-data.service';
|
import {ShowDataService} from './show-data.service';
|
||||||
import {ShowService} from './show.service';
|
import {ShowService} from './show.service';
|
||||||
import {UserService} from '../../../services/user/user.service';
|
import {UserService} from '../../../services/user/user.service';
|
||||||
@@ -8,6 +8,7 @@ describe('ShowService', () => {
|
|||||||
let service: ShowService;
|
let service: ShowService;
|
||||||
let showDataServiceSpy: jasmine.SpyObj<ShowDataService>;
|
let showDataServiceSpy: jasmine.SpyObj<ShowDataService>;
|
||||||
let user$: BehaviorSubject<unknown>;
|
let user$: BehaviorSubject<unknown>;
|
||||||
|
let shows$: BehaviorSubject<unknown[]>;
|
||||||
const shows = [
|
const shows = [
|
||||||
{id: 'show-1', owner: 'user-1', published: false, archived: false},
|
{id: 'show-1', owner: 'user-1', published: false, archived: false},
|
||||||
{id: 'show-2', owner: 'other-user', published: true, archived: false},
|
{id: 'show-2', owner: 'other-user', published: true, archived: false},
|
||||||
@@ -16,8 +17,9 @@ describe('ShowService', () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
user$ = new BehaviorSubject<unknown>({id: 'user-1'});
|
user$ = new BehaviorSubject<unknown>({id: 'user-1'});
|
||||||
|
shows$ = new BehaviorSubject<unknown[]>(shows as unknown[]);
|
||||||
showDataServiceSpy = jasmine.createSpyObj<ShowDataService>('ShowDataService', ['read$', 'listPublicSince$', 'update', 'add'], {
|
showDataServiceSpy = jasmine.createSpyObj<ShowDataService>('ShowDataService', ['read$', 'listPublicSince$', 'update', 'add'], {
|
||||||
list$: of(shows) as unknown as ShowDataService['list$'],
|
list$: shows$.asObservable() as unknown as ShowDataService['list$'],
|
||||||
});
|
});
|
||||||
showDataServiceSpy.read$.and.returnValue(of(shows[0]));
|
showDataServiceSpy.read$.and.returnValue(of(shows[0]));
|
||||||
showDataServiceSpy.listPublicSince$.and.returnValue(of([shows[1]]));
|
showDataServiceSpy.listPublicSince$.and.returnValue(of([shows[1]]));
|
||||||
@@ -38,34 +40,39 @@ describe('ShowService', () => {
|
|||||||
expect(service).toBeTruthy();
|
expect(service).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should list published shows and own drafts, but exclude archived ones', done => {
|
it('should list published shows and own drafts, but exclude archived ones', async () => {
|
||||||
service.list$().subscribe(result => {
|
const result = await firstValueFrom(service.list$());
|
||||||
expect(result.map(show => show.id)).toEqual(['show-1', 'show-2']);
|
expect(result.map(show => show.id)).toEqual(['show-1', 'show-2']);
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should filter out private drafts when publishedOnly is true', done => {
|
it('should filter out private drafts when publishedOnly is true', async () => {
|
||||||
service.list$(true).subscribe(result => {
|
const result = await firstValueFrom(service.list$(true));
|
||||||
expect(result.map(show => show.id)).toEqual(['show-2']);
|
expect(result.map(show => show.id)).toEqual(['show-2']);
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should delegate public listing to the data service', done => {
|
it('should include own archived shows when requested', async () => {
|
||||||
service.listPublicSince$(6).subscribe(result => {
|
const result = await firstValueFrom(service.list$(false, true));
|
||||||
expect(result).toEqual([shows[1]]);
|
expect(result.map(show => show.id)).toEqual(['show-1', 'show-2', 'show-3']);
|
||||||
expect(showDataServiceSpy.listPublicSince$).toHaveBeenCalledWith(6);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should delegate reads to the data service', done => {
|
it('should not include archived shows from other users when requested', async () => {
|
||||||
service.read$('show-1').subscribe(result => {
|
shows$.next([
|
||||||
expect(result).toEqual(shows[0]);
|
...(shows as unknown as unknown[]),
|
||||||
expect(showDataServiceSpy.read$).toHaveBeenCalledWith('show-1');
|
{id: 'show-4', owner: 'other-user', published: true, archived: true},
|
||||||
done();
|
]);
|
||||||
});
|
|
||||||
|
const result = await firstValueFrom(service.list$(false, true));
|
||||||
|
expect(result.map(show => show.id)).toEqual(['show-1', 'show-2', 'show-3']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delegate public listing to the data service', async () => {
|
||||||
|
await expectAsync(firstValueFrom(service.listPublicSince$(6))).toBeResolvedTo([shows[1]]);
|
||||||
|
expect(showDataServiceSpy.listPublicSince$).toHaveBeenCalledWith(6);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delegate reads to the data service', async () => {
|
||||||
|
await expectAsync(firstValueFrom(service.read$('show-1'))).toBeResolvedTo(shows[0]);
|
||||||
|
expect(showDataServiceSpy.read$).toHaveBeenCalledWith('show-1');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should delegate updates to the data service', async () => {
|
it('should delegate updates to the data service', async () => {
|
||||||
|
|||||||
@@ -20,13 +20,17 @@ export class ShowService {
|
|||||||
public read$ = (showId: string): Observable<Show | null> => this.showDataService.read$(showId);
|
public read$ = (showId: string): Observable<Show | null> => this.showDataService.read$(showId);
|
||||||
public listPublicSince$ = (lastMonths: number): Observable<Show[]> => this.showDataService.listPublicSince$(lastMonths);
|
public listPublicSince$ = (lastMonths: number): Observable<Show[]> => this.showDataService.listPublicSince$(lastMonths);
|
||||||
|
|
||||||
public list$(publishedOnly = false): Observable<Show[]> {
|
public list$(publishedOnly = false, includeOwnArchived = false): Observable<Show[]> {
|
||||||
return this.userService.user$.pipe(
|
return this.userService.user$.pipe(
|
||||||
switchMap(
|
switchMap(
|
||||||
() => this.showDataService.list$,
|
() => this.showDataService.list$,
|
||||||
(user: User | null, shows: Show[]) => ({user, shows})
|
(user: User | null, shows: Show[]) => ({user, shows})
|
||||||
),
|
),
|
||||||
map(s => s.shows.filter(show => !show.archived).filter(show => show.published || (show.owner === s.user?.id && !publishedOnly)))
|
map(s =>
|
||||||
|
s.shows
|
||||||
|
.filter(show => !show.archived || (includeOwnArchived && show.owner === s.user?.id))
|
||||||
|
.filter(show => show.published || (show.owner === s.user?.id && !publishedOnly))
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {SongComponent} from './song.component';
|
import {SongComponent} from './song.component';
|
||||||
|
|
||||||
@@ -6,11 +6,11 @@ describe('SongComponent', () => {
|
|||||||
let component: SongComponent;
|
let component: SongComponent;
|
||||||
let fixture: ComponentFixture<SongComponent>;
|
let fixture: ComponentFixture<SongComponent>;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [SongComponent],
|
imports: [SongComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(SongComponent);
|
fixture = TestBed.createComponent(SongComponent);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {NewComponent} from './new/new.component';
|
|||||||
import {ListComponent} from './list/list.component';
|
import {ListComponent} from './list/list.component';
|
||||||
import {ShowComponent} from './show/show.component';
|
import {ShowComponent} from './show/show.component';
|
||||||
import {EditComponent} from './edit/edit.component';
|
import {EditComponent} from './edit/edit.component';
|
||||||
|
import {RoleGuard} from '../../widget-modules/guards/role.guard';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
@@ -14,6 +15,10 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: 'new',
|
path: 'new',
|
||||||
component: NewComponent,
|
component: NewComponent,
|
||||||
|
canActivate: [RoleGuard],
|
||||||
|
data: {
|
||||||
|
requiredRoles: ['leader'],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ':showId/edit',
|
path: ':showId/edit',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {TestBed} from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
import {of} from 'rxjs';
|
import {firstValueFrom, of} from 'rxjs';
|
||||||
import {SongService} from './song.service';
|
import {SongService} from './song.service';
|
||||||
import {SongListResolver} from './song-list.resolver';
|
import {SongListResolver} from './song-list.resolver';
|
||||||
|
|
||||||
@@ -22,11 +22,8 @@ describe('SongListResolver', () => {
|
|||||||
expect(resolver).toBeTruthy();
|
expect(resolver).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should resolve the first emitted song list from the service', done => {
|
it('should resolve the first emitted song list from the service', async () => {
|
||||||
resolver.resolve().subscribe(songs => {
|
await expectAsync(firstValueFrom(resolver.resolve())).toBeResolvedTo([{id: 'song-1', title: 'Amazing Grace'}] as never);
|
||||||
expect(songServiceSpy.listLoaded$).toHaveBeenCalled();
|
expect(songServiceSpy.listLoaded$).toHaveBeenCalled();
|
||||||
expect(songs).toEqual([{id: 'song-1', title: 'Amazing Grace'}] as never);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {TestBed} from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
import {of} from 'rxjs';
|
import {firstValueFrom, of} from 'rxjs';
|
||||||
import {SongDataService} from './song-data.service';
|
import {SongDataService} from './song-data.service';
|
||||||
import {SongService} from './song.service';
|
import {SongService} from './song.service';
|
||||||
import {UserService} from '../../../services/user/user.service';
|
import {UserService} from '../../../services/user/user.service';
|
||||||
@@ -41,11 +41,8 @@ describe('SongService', () => {
|
|||||||
expect(service).toBeTruthy();
|
expect(service).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should list songs from the data service', done => {
|
it('should list songs from the data service', async () => {
|
||||||
service.list$().subscribe(songs => {
|
await expectAsync(firstValueFrom(service.list$())).toBeResolvedTo([song]);
|
||||||
expect(songs).toEqual([song]);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should delegate reads to the data service', async () => {
|
it('should delegate reads to the data service', async () => {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ Bridge
|
|||||||
Cool bridge without any chords
|
Cool bridge without any chords
|
||||||
`;
|
`;
|
||||||
|
|
||||||
beforeEach(() => void TestBed.configureTestingModule({}));
|
beforeEach(async () => await TestBed.configureTestingModule({}));
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
||||||
const service: TextRenderingService = TestBed.inject(TextRenderingService);
|
const service: TextRenderingService = TestBed.inject(TextRenderingService);
|
||||||
@@ -478,13 +478,13 @@ Text`;
|
|||||||
const service: TextRenderingService = TestBed.inject(TextRenderingService);
|
const service: TextRenderingService = TestBed.inject(TextRenderingService);
|
||||||
const text = 'Strophe\nC\tG\ta\nText';
|
const text = 'Strophe\nC\tG\ta\nText';
|
||||||
|
|
||||||
void expect(service.validateChordNotation(text)).toContain(
|
void expect(service.validateChordNotation(text)).toEqual(expect.arrayContaining([
|
||||||
jasmine.objectContaining({
|
jasmine.objectContaining({
|
||||||
lineNumber: 2,
|
lineNumber: 2,
|
||||||
token: '\t',
|
token: '\t',
|
||||||
reason: 'tab_character',
|
reason: 'tab_character',
|
||||||
})
|
}),
|
||||||
);
|
]));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not flag tabs on non chord lines', () => {
|
it('should not flag tabs on non chord lines', () => {
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import {Line} from './line';
|
|||||||
describe('TransposeService', () => {
|
describe('TransposeService', () => {
|
||||||
let service: TransposeService;
|
let service: TransposeService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({});
|
await TestBed.configureTestingModule({});
|
||||||
service = TestBed.inject(TransposeService);
|
service = TestBed.inject(TransposeService);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
.third {
|
.third,
|
||||||
display: grid;
|
:host ::ng-deep form,
|
||||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
div[formGroup] {
|
||||||
column-gap: 20px;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.third {
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .mat-mdc-form-field {
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {FilterComponent} from './filter.component';
|
import {FilterComponent} from './filter.component';
|
||||||
|
|
||||||
@@ -6,11 +6,11 @@ describe('FilterComponent', () => {
|
|||||||
let component: FilterComponent;
|
let component: FilterComponent;
|
||||||
let fixture: ComponentFixture<FilterComponent>;
|
let fixture: ComponentFixture<FilterComponent>;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [FilterComponent],
|
imports: [FilterComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(FilterComponent);
|
fixture = TestBed.createComponent(FilterComponent);
|
||||||
|
|||||||
@@ -1,43 +1,48 @@
|
|||||||
@if (songs$ | async; as songs) {
|
@if (songs$ | async; as songs) {
|
||||||
<div>
|
<app-sidebar>
|
||||||
<app-list-header [anyFilterActive]="anyFilterActive">
|
<div sidebar class="sidebar-content">
|
||||||
<app-filter [songs]="songs"></app-filter>
|
<app-filter [songs]="songs"></app-filter>
|
||||||
</app-list-header>
|
</div>
|
||||||
<app-card [padding]="false">
|
<div content>
|
||||||
@for (song of songs; track trackBy($index, song)) {
|
<app-card [padding]="false">
|
||||||
<div [routerLink]="song.id" class="list-item">
|
@for (song of songs; track trackBy($index, song)) {
|
||||||
<div class="number">{{ song.number }}</div>
|
<div [routerLink]="song.id" class="list-item">
|
||||||
<div class="title">
|
<div class="number">{{ song.number }}</div>
|
||||||
<span>{{ song.title }}</span>
|
<div class="title">
|
||||||
@if (song.hasChordValidationIssues) {
|
<span>{{ song.title }}</span>
|
||||||
<span class="validation-star" title="Akkord-Validierungsfehler vorhanden">*</span>
|
@if (song.hasChordValidationIssues) {
|
||||||
}
|
<span class="validation-star" title="Akkord-Validierungsfehler vorhanden">*</span>
|
||||||
</div>
|
}
|
||||||
<div>
|
</div>
|
||||||
<ng-container *appRole="['contributor']">
|
<div>
|
||||||
@if (song.status === 'draft') {
|
<ng-container *appRole="['contributor']">
|
||||||
|
@if (song.status === 'draft') {
|
||||||
|
<div class="warning">
|
||||||
|
<fa-icon [icon]="faDraft"></fa-icon>
|
||||||
|
</div>
|
||||||
|
} @if (song.status === 'set') {
|
||||||
|
<div class="neutral">
|
||||||
|
<fa-icon [icon]="faDraft"></fa-icon>
|
||||||
|
</div>
|
||||||
|
} @if (song.status === 'final') {
|
||||||
|
<div class="success">
|
||||||
|
<fa-icon [icon]="faFinal"></fa-icon>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</ng-container>
|
||||||
|
@if (song.legalType === 'open') {
|
||||||
<div class="warning">
|
<div class="warning">
|
||||||
<fa-icon [icon]="faDraft"></fa-icon>
|
<fa-icon [icon]="faLegal"></fa-icon>
|
||||||
</div>
|
|
||||||
} @if (song.status === 'set') {
|
|
||||||
<div class="neutral">
|
|
||||||
<fa-icon [icon]="faDraft"></fa-icon>
|
|
||||||
</div>
|
|
||||||
} @if (song.status === 'final') {
|
|
||||||
<div class="success">
|
|
||||||
<fa-icon [icon]="faFinal"></fa-icon>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</ng-container>
|
|
||||||
@if (song.legalType === 'open') {
|
|
||||||
<div class="warning">
|
|
||||||
<fa-icon [icon]="faLegal"></fa-icon>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
<div>{{ song.key }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>{{ song.key }}</div>
|
}
|
||||||
</div>
|
<div *appRole="['contributor']" class="list-action">
|
||||||
}
|
<app-button [fullWidth]="true" [icon]="faNewSong" routerLink="new">Neuen Song anlegen</app-button>
|
||||||
</app-card>
|
</div>
|
||||||
</div>
|
</app-card>
|
||||||
|
</div>
|
||||||
|
</app-sidebar>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
.sidebar-content {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.list-item {
|
.list-item {
|
||||||
padding: 5px 20px;
|
padding: 5px 20px;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
import {SongListComponent} from './song-list.component';
|
import {SongListComponent} from './song-list.component';
|
||||||
import {of} from 'rxjs';
|
import {of} from 'rxjs';
|
||||||
import {ActivatedRoute} from '@angular/router';
|
import {ActivatedRoute} from '@angular/router';
|
||||||
@@ -12,8 +12,8 @@ describe('SongListComponent', () => {
|
|||||||
|
|
||||||
const songs = [{id: 'song-1', title: 'title1', number: 1, text: '', flags: ''}];
|
const songs = [{id: 'song-1', title: 'title1', number: 1, text: '', flags: ''}];
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [SongListComponent],
|
imports: [SongListComponent],
|
||||||
providers: [
|
providers: [
|
||||||
{provide: ActivatedRoute, useValue: {data: of({songs})}},
|
{provide: ActivatedRoute, useValue: {data: of({songs})}},
|
||||||
@@ -22,7 +22,7 @@ describe('SongListComponent', () => {
|
|||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA],
|
schemas: [NO_ERRORS_SCHEMA],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(SongListComponent);
|
fixture = TestBed.createComponent(SongListComponent);
|
||||||
|
|||||||
@@ -6,15 +6,16 @@ import {fade} from '../../../animations';
|
|||||||
import {ActivatedRoute, RouterLink} from '@angular/router';
|
import {ActivatedRoute, RouterLink} from '@angular/router';
|
||||||
import {filterSong} from '../../../services/filter.helper';
|
import {filterSong} from '../../../services/filter.helper';
|
||||||
import {FilterValues} from './filter/filter-values';
|
import {FilterValues} from './filter/filter-values';
|
||||||
import {faBalanceScaleRight, faCheck, faPencilRuler} from '@fortawesome/free-solid-svg-icons';
|
import {faBalanceScaleRight, faCheck, faPencilRuler, faPlus} from '@fortawesome/free-solid-svg-icons';
|
||||||
import {TextRenderingService} from '../services/text-rendering.service';
|
import {TextRenderingService} from '../services/text-rendering.service';
|
||||||
import {FilterStoreService} from '../../../services/filter-store.service';
|
import {FilterStoreService} from '../../../services/filter-store.service';
|
||||||
import {AsyncPipe} from '@angular/common';
|
import {AsyncPipe} from '@angular/common';
|
||||||
import {ListHeaderComponent} from '../../../widget-modules/components/list-header/list-header.component';
|
|
||||||
import {FilterComponent} from './filter/filter.component';
|
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 {RoleDirective} from '../../../services/user/role.directive';
|
import {RoleDirective} from '../../../services/user/role.directive';
|
||||||
import {FaIconComponent} from '@fortawesome/angular-fontawesome';
|
import {FaIconComponent} from '@fortawesome/angular-fontawesome';
|
||||||
|
import {SidebarComponent} from '../../../widget-modules/components/sidebar/sidebar.component';
|
||||||
|
import {ButtonComponent} from '../../../widget-modules/components/button/button.component';
|
||||||
|
|
||||||
interface SongListItem extends Song {
|
interface SongListItem extends Song {
|
||||||
hasChordValidationIssues: boolean;
|
hasChordValidationIssues: boolean;
|
||||||
@@ -26,20 +27,21 @@ interface SongListItem extends Song {
|
|||||||
styleUrls: ['./song-list.component.less'],
|
styleUrls: ['./song-list.component.less'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
animations: [fade],
|
animations: [fade],
|
||||||
imports: [ListHeaderComponent, FilterComponent, CardComponent, RouterLink, RoleDirective, FaIconComponent, AsyncPipe],
|
imports: [FilterComponent, CardComponent, RouterLink, RoleDirective, FaIconComponent, AsyncPipe, SidebarComponent, ButtonComponent],
|
||||||
})
|
})
|
||||||
export class SongListComponent {
|
export class SongListComponent {
|
||||||
|
public faLegal = faBalanceScaleRight;
|
||||||
|
public faDraft = faPencilRuler;
|
||||||
|
public faFinal = faCheck;
|
||||||
|
public faNewSong = faPlus;
|
||||||
private route = inject(ActivatedRoute);
|
private route = inject(ActivatedRoute);
|
||||||
private textRenderingService = inject(TextRenderingService);
|
private textRenderingService = inject(TextRenderingService);
|
||||||
private filterStore = inject(FilterStoreService);
|
private filterStore = inject(FilterStoreService);
|
||||||
|
|
||||||
public anyFilterActive = false;
|
|
||||||
public songs$: Observable<SongListItem[]> = combineLatest([
|
public songs$: Observable<SongListItem[]> = combineLatest([
|
||||||
this.filterStore.songFilter$,
|
this.filterStore.songFilter$,
|
||||||
this.route.data.pipe(map(data => (data['songs'] as Song[]).slice().sort((a, b) => a.number - b.number))),
|
this.route.data.pipe(map(data => (data['songs'] as Song[]).slice().sort((a, b) => a.number - b.number))),
|
||||||
]).pipe(
|
]).pipe(
|
||||||
map(([filter, songs]) => {
|
map(([filter, songs]) => {
|
||||||
this.anyFilterActive = this.checkIfFilterActive(filter);
|
|
||||||
return songs
|
return songs
|
||||||
.filter(song => this.filter(song, filter))
|
.filter(song => this.filter(song, filter))
|
||||||
.map(song => ({
|
.map(song => ({
|
||||||
@@ -49,9 +51,6 @@ export class SongListComponent {
|
|||||||
.sort((a, b) => a.title?.localeCompare(b.title));
|
.sort((a, b) => a.title?.localeCompare(b.title));
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
public faLegal = faBalanceScaleRight;
|
|
||||||
public faDraft = faPencilRuler;
|
|
||||||
public faFinal = faCheck;
|
|
||||||
|
|
||||||
public trackBy = (index: number, show: SongListItem) => show.id;
|
public trackBy = (index: number, show: SongListItem) => show.id;
|
||||||
|
|
||||||
@@ -65,10 +64,6 @@ export class SongListComponent {
|
|||||||
return baseFilter;
|
return baseFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
private checkIfFilterActive(filter: FilterValues): boolean {
|
|
||||||
return !!filter.q || !!filter.type || !!filter.key || !!filter.legalType || !!filter.flag;
|
|
||||||
}
|
|
||||||
|
|
||||||
private checkFlag(flag: string, flags: string) {
|
private checkFlag(flag: string, flags: string) {
|
||||||
if (!flags) {
|
if (!flags) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {EditFileComponent} from './edit-file.component';
|
import {EditFileComponent} from './edit-file.component';
|
||||||
|
|
||||||
@@ -6,11 +6,11 @@ describe('EditFileComponent', () => {
|
|||||||
let component: EditFileComponent;
|
let component: EditFileComponent;
|
||||||
let fixture: ComponentFixture<EditFileComponent>;
|
let fixture: ComponentFixture<EditFileComponent>;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [EditFileComponent],
|
imports: [EditFileComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(EditFileComponent);
|
fixture = TestBed.createComponent(EditFileComponent);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {FileComponent} from './file.component';
|
import {FileComponent} from './file.component';
|
||||||
|
|
||||||
@@ -6,11 +6,11 @@ describe('FileComponent', () => {
|
|||||||
let component: FileComponent;
|
let component: FileComponent;
|
||||||
let fixture: ComponentFixture<FileComponent>;
|
let fixture: ComponentFixture<FileComponent>;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [FileComponent],
|
imports: [FileComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(FileComponent);
|
fixture = TestBed.createComponent(FileComponent);
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import {EditSongGuard} from './edit-song.guard';
|
|||||||
describe('EditSongGuard', () => {
|
describe('EditSongGuard', () => {
|
||||||
let guard: EditSongGuard;
|
let guard: EditSongGuard;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({});
|
await TestBed.configureTestingModule({});
|
||||||
guard = TestBed.inject(EditSongGuard);
|
guard = TestBed.inject(EditSongGuard);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {EditSongComponent} from './edit-song.component';
|
import {EditSongComponent} from './edit-song.component';
|
||||||
|
|
||||||
@@ -6,11 +6,11 @@ describe('EditSongComponent', () => {
|
|||||||
let component: EditSongComponent;
|
let component: EditSongComponent;
|
||||||
let fixture: ComponentFixture<EditSongComponent>;
|
let fixture: ComponentFixture<EditSongComponent>;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [EditSongComponent],
|
imports: [EditSongComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(EditSongComponent);
|
fixture = TestBed.createComponent(EditSongComponent);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {SaveDialogComponent} from './save-dialog.component';
|
import {SaveDialogComponent} from './save-dialog.component';
|
||||||
|
|
||||||
@@ -6,11 +6,11 @@ describe('SaveDialogComponent', () => {
|
|||||||
let component: SaveDialogComponent;
|
let component: SaveDialogComponent;
|
||||||
let fixture: ComponentFixture<SaveDialogComponent>;
|
let fixture: ComponentFixture<SaveDialogComponent>;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [SaveDialogComponent],
|
imports: [SaveDialogComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(SaveDialogComponent);
|
fixture = TestBed.createComponent(SaveDialogComponent);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {EditComponent} from './edit.component';
|
import {EditComponent} from './edit.component';
|
||||||
|
|
||||||
@@ -6,11 +6,11 @@ describe('EditComponent', () => {
|
|||||||
let component: EditComponent;
|
let component: EditComponent;
|
||||||
let fixture: ComponentFixture<EditComponent>;
|
let fixture: ComponentFixture<EditComponent>;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [EditComponent],
|
imports: [EditComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(EditComponent);
|
fixture = TestBed.createComponent(EditComponent);
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import {EditService} from './edit.service';
|
|||||||
describe('EditService', () => {
|
describe('EditService', () => {
|
||||||
let service: EditService;
|
let service: EditService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({});
|
await TestBed.configureTestingModule({});
|
||||||
service = TestBed.inject(EditService);
|
service = TestBed.inject(EditService);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {HistoryComponent} from './history.component';
|
import {HistoryComponent} from './history.component';
|
||||||
|
|
||||||
@@ -6,11 +6,11 @@ describe('HistoryComponent', () => {
|
|||||||
let component: HistoryComponent;
|
let component: HistoryComponent;
|
||||||
let fixture: ComponentFixture<HistoryComponent>;
|
let fixture: ComponentFixture<HistoryComponent>;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [HistoryComponent],
|
imports: [HistoryComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(HistoryComponent);
|
fixture = TestBed.createComponent(HistoryComponent);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
import {Storage} from '@angular/fire/storage';
|
import {Storage} from '@angular/fire/storage';
|
||||||
|
|
||||||
import {FileComponent} from './file.component';
|
import {FileComponent} from './file.component';
|
||||||
@@ -7,12 +7,12 @@ describe('FileComponent', () => {
|
|||||||
let component: FileComponent;
|
let component: FileComponent;
|
||||||
let fixture: ComponentFixture<FileComponent>;
|
let fixture: ComponentFixture<FileComponent>;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [FileComponent],
|
imports: [FileComponent],
|
||||||
providers: [{provide: Storage, useValue: {}}],
|
providers: [{provide: Storage, useValue: {}}],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(FileComponent);
|
fixture = TestBed.createComponent(FileComponent);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {NewComponent} from './new.component';
|
import {NewComponent} from './new.component';
|
||||||
|
|
||||||
@@ -6,11 +6,11 @@ describe('NewComponent', () => {
|
|||||||
let component: NewComponent;
|
let component: NewComponent;
|
||||||
let fixture: ComponentFixture<NewComponent>;
|
let fixture: ComponentFixture<NewComponent>;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [NewComponent],
|
imports: [NewComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(NewComponent);
|
fixture = TestBed.createComponent(NewComponent);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {SongComponent} from './song.component';
|
import {SongComponent} from './song.component';
|
||||||
import {of} from 'rxjs';
|
import {of} from 'rxjs';
|
||||||
@@ -17,7 +17,7 @@ describe('SongComponent', () => {
|
|||||||
params: of({songId: '4711'}),
|
params: of({songId: '4711'}),
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
const songServiceSpy = jasmine.createSpyObj<SongService>('SongService', ['read$']);
|
const songServiceSpy = jasmine.createSpyObj<SongService>('SongService', ['read$']);
|
||||||
const fileDataServiceSpy = jasmine.createSpyObj<FileDataService>('FileDataService', ['read$']);
|
const fileDataServiceSpy = jasmine.createSpyObj<FileDataService>('FileDataService', ['read$']);
|
||||||
const userServiceSpy = jasmine.createSpyObj<UserService>('UserService', ['incSongCount', 'decSongCount'], {
|
const userServiceSpy = jasmine.createSpyObj<UserService>('UserService', ['incSongCount', 'decSongCount'], {
|
||||||
@@ -33,7 +33,7 @@ describe('SongComponent', () => {
|
|||||||
userServiceSpy.loggedIn$ = jasmine.createSpy('loggedIn$').and.returnValue(of(true));
|
userServiceSpy.loggedIn$ = jasmine.createSpy('loggedIn$').and.returnValue(of(true));
|
||||||
userServiceSpy.getUserbyId$ = jasmine.createSpy('getUserbyId$').and.returnValue(of({name: 'Benjamin'}));
|
userServiceSpy.getUserbyId$ = jasmine.createSpy('getUserbyId$').and.returnValue(of({name: 'Benjamin'}));
|
||||||
|
|
||||||
void TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [SongComponent],
|
imports: [SongComponent],
|
||||||
providers: [
|
providers: [
|
||||||
{provide: ActivatedRoute, useValue: mockActivatedRoute},
|
{provide: ActivatedRoute, useValue: mockActivatedRoute},
|
||||||
@@ -44,7 +44,7 @@ describe('SongComponent', () => {
|
|||||||
{provide: ShowSongService, useValue: showSongServiceSpy},
|
{provide: ShowSongService, useValue: showSongServiceSpy},
|
||||||
],
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(SongComponent);
|
fixture = TestBed.createComponent(SongComponent);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {EditComponent} from './song/edit/edit.component';
|
|||||||
import {NewComponent} from './song/new/new.component';
|
import {NewComponent} from './song/new/new.component';
|
||||||
import {EditSongGuard} from './song/edit/edit-song.guard';
|
import {EditSongGuard} from './song/edit/edit-song.guard';
|
||||||
import {SongListResolver} from './services/song-list.resolver';
|
import {SongListResolver} from './services/song-list.resolver';
|
||||||
|
import {RoleGuard} from '../../widget-modules/guards/role.guard';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
@@ -19,6 +20,10 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: 'new',
|
path: 'new',
|
||||||
component: NewComponent,
|
component: NewComponent,
|
||||||
|
canActivate: [RoleGuard],
|
||||||
|
data: {
|
||||||
|
requiredRoles: ['contributor'],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ':songId/edit',
|
path: ':songId/edit',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {InfoComponent} from './info.component';
|
import {InfoComponent} from './info.component';
|
||||||
|
|
||||||
@@ -6,11 +6,11 @@ describe('InfoComponent', () => {
|
|||||||
let component: InfoComponent;
|
let component: InfoComponent;
|
||||||
let fixture: ComponentFixture<InfoComponent>;
|
let fixture: ComponentFixture<InfoComponent>;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [InfoComponent],
|
imports: [InfoComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(InfoComponent);
|
fixture = TestBed.createComponent(InfoComponent);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {UserComponent} from './user.component';
|
import {UserComponent} from './user.component';
|
||||||
|
|
||||||
@@ -6,11 +6,11 @@ describe('UserComponent', () => {
|
|||||||
let component: UserComponent;
|
let component: UserComponent;
|
||||||
let fixture: ComponentFixture<UserComponent>;
|
let fixture: ComponentFixture<UserComponent>;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [UserComponent],
|
imports: [UserComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(UserComponent);
|
fixture = TestBed.createComponent(UserComponent);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {UsersComponent} from './users.component';
|
import {UsersComponent} from './users.component';
|
||||||
|
|
||||||
@@ -6,11 +6,11 @@ describe('UsersComponent', () => {
|
|||||||
let component: UsersComponent;
|
let component: UsersComponent;
|
||||||
let fixture: ComponentFixture<UsersComponent>;
|
let fixture: ComponentFixture<UsersComponent>;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [UsersComponent],
|
imports: [UsersComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(UsersComponent);
|
fixture = TestBed.createComponent(UsersComponent);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {LoginComponent} from './login.component';
|
import {LoginComponent} from './login.component';
|
||||||
|
|
||||||
@@ -6,11 +6,11 @@ describe('LoginComponent', () => {
|
|||||||
let component: LoginComponent;
|
let component: LoginComponent;
|
||||||
let fixture: ComponentFixture<LoginComponent>;
|
let fixture: ComponentFixture<LoginComponent>;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [LoginComponent],
|
imports: [LoginComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(LoginComponent);
|
fixture = TestBed.createComponent(LoginComponent);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {LogoutComponent} from './logout.component';
|
import {LogoutComponent} from './logout.component';
|
||||||
|
|
||||||
@@ -6,11 +6,11 @@ describe('LogoutComponent', () => {
|
|||||||
let component: LogoutComponent;
|
let component: LogoutComponent;
|
||||||
let fixture: ComponentFixture<LogoutComponent>;
|
let fixture: ComponentFixture<LogoutComponent>;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [LogoutComponent],
|
imports: [LogoutComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(LogoutComponent);
|
fixture = TestBed.createComponent(LogoutComponent);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {NewComponent} from './new.component';
|
import {NewComponent} from './new.component';
|
||||||
|
|
||||||
@@ -6,11 +6,11 @@ describe('NewComponent', () => {
|
|||||||
let component: NewComponent;
|
let component: NewComponent;
|
||||||
let fixture: ComponentFixture<NewComponent>;
|
let fixture: ComponentFixture<NewComponent>;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [NewComponent],
|
imports: [NewComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(NewComponent);
|
fixture = TestBed.createComponent(NewComponent);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {PasswordSendComponent} from './password-send.component';
|
import {PasswordSendComponent} from './password-send.component';
|
||||||
|
|
||||||
@@ -6,11 +6,11 @@ describe('PasswordSendComponent', () => {
|
|||||||
let component: PasswordSendComponent;
|
let component: PasswordSendComponent;
|
||||||
let fixture: ComponentFixture<PasswordSendComponent>;
|
let fixture: ComponentFixture<PasswordSendComponent>;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [PasswordSendComponent],
|
imports: [PasswordSendComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(PasswordSendComponent);
|
fixture = TestBed.createComponent(PasswordSendComponent);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {PasswordComponent} from './password.component';
|
import {PasswordComponent} from './password.component';
|
||||||
|
|
||||||
@@ -6,11 +6,11 @@ describe('PasswordComponent', () => {
|
|||||||
let component: PasswordComponent;
|
let component: PasswordComponent;
|
||||||
let fixture: ComponentFixture<PasswordComponent>;
|
let fixture: ComponentFixture<PasswordComponent>;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [PasswordComponent],
|
imports: [PasswordComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(PasswordComponent);
|
fixture = TestBed.createComponent(PasswordComponent);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {TestBed} from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
import {of} from 'rxjs';
|
import {firstValueFrom, of} from 'rxjs';
|
||||||
import {DbService} from './db.service';
|
import {DbService} from './db.service';
|
||||||
import {ConfigService} from './config.service';
|
import {ConfigService} from './config.service';
|
||||||
|
|
||||||
@@ -27,11 +27,8 @@ describe('ConfigService', () => {
|
|||||||
expect(dbServiceSpy.doc$).toHaveBeenCalledWith('global/config');
|
expect(dbServiceSpy.doc$).toHaveBeenCalledWith('global/config');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should expose the shared config stream via get$', done => {
|
it('should expose the shared config stream via get$', async () => {
|
||||||
service.get$().subscribe(config => {
|
await expectAsync(firstValueFrom(service.get$())).toBeResolvedTo({copyright: 'CCLI'} as never);
|
||||||
expect(config).toEqual({copyright: 'CCLI'} as never);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should resolve the current config via get()', async () => {
|
it('should resolve the current config via get()', async () => {
|
||||||
|
|||||||
@@ -4,12 +4,11 @@ import {Firestore} from '@angular/fire/firestore';
|
|||||||
import {DbService} from './db.service';
|
import {DbService} from './db.service';
|
||||||
|
|
||||||
describe('DbService', () => {
|
describe('DbService', () => {
|
||||||
beforeEach(
|
beforeEach(async () => {
|
||||||
() =>
|
await TestBed.configureTestingModule({
|
||||||
void TestBed.configureTestingModule({
|
providers: [{provide: Firestore, useValue: {}}],
|
||||||
providers: [{provide: Firestore, useValue: {}}],
|
});
|
||||||
})
|
});
|
||||||
);
|
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
||||||
const service: DbService = TestBed.inject(DbService);
|
const service: DbService = TestBed.inject(DbService);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const DEFAULT_SHOW_FILTER: ShowFilterValues = {
|
|||||||
time: 1,
|
time: 1,
|
||||||
owner: '',
|
owner: '',
|
||||||
showType: '',
|
showType: '',
|
||||||
|
archived: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {TestBed} from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
import {of} from 'rxjs';
|
import {firstValueFrom, of} from 'rxjs';
|
||||||
import {DbService} from './db.service';
|
import {DbService} from './db.service';
|
||||||
import {GlobalSettingsService} from './global-settings.service';
|
import {GlobalSettingsService} from './global-settings.service';
|
||||||
|
|
||||||
@@ -30,11 +30,8 @@ describe('GlobalSettingsService', () => {
|
|||||||
expect(dbServiceSpy.doc$).toHaveBeenCalledWith('global/static');
|
expect(dbServiceSpy.doc$).toHaveBeenCalledWith('global/static');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should expose the shared settings stream via the getter', done => {
|
it('should expose the shared settings stream via the getter', async () => {
|
||||||
service.get$.subscribe(settings => {
|
await expectAsync(firstValueFrom(service.get$)).toBeResolvedTo({churchName: 'ICF'} as never);
|
||||||
expect(settings).toEqual({churchName: 'ICF'} as never);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update the static global settings document', async () => {
|
it('should update the static global settings document', async () => {
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import {ScrollService} from './scroll.service';
|
|||||||
describe('ScrollService', () => {
|
describe('ScrollService', () => {
|
||||||
let service: ScrollService;
|
let service: ScrollService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({});
|
await TestBed.configureTestingModule({});
|
||||||
service = TestBed.inject(ScrollService);
|
service = TestBed.inject(ScrollService);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
import {of} from 'rxjs';
|
import {firstValueFrom, of} from 'rxjs';
|
||||||
import {UserService} from '../user.service';
|
import {UserService} from '../user.service';
|
||||||
import {UserNameComponent} from './user-name.component';
|
import {UserNameComponent} from './user-name.component';
|
||||||
|
|
||||||
@@ -8,15 +8,15 @@ describe('UserNameComponent', () => {
|
|||||||
let fixture: ComponentFixture<UserNameComponent>;
|
let fixture: ComponentFixture<UserNameComponent>;
|
||||||
let userServiceSpy: jasmine.SpyObj<UserService>;
|
let userServiceSpy: jasmine.SpyObj<UserService>;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
userServiceSpy = jasmine.createSpyObj<UserService>('UserService', ['getUserbyId$']);
|
userServiceSpy = jasmine.createSpyObj<UserService>('UserService', ['getUserbyId$']);
|
||||||
userServiceSpy.getUserbyId$.and.returnValue(of({name: 'Benjamin'} as never));
|
userServiceSpy.getUserbyId$.and.returnValue(of({name: 'Benjamin'} as never));
|
||||||
|
|
||||||
void TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [UserNameComponent],
|
imports: [UserNameComponent],
|
||||||
providers: [{provide: UserService, useValue: userServiceSpy}],
|
providers: [{provide: UserService, useValue: userServiceSpy}],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(UserNameComponent);
|
fixture = TestBed.createComponent(UserNameComponent);
|
||||||
@@ -28,24 +28,18 @@ describe('UserNameComponent', () => {
|
|||||||
expect(component).toBeTruthy();
|
expect(component).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should resolve the user name when the userId input changes', done => {
|
it('should resolve the user name when the userId input changes', async () => {
|
||||||
component.userId = 'user-1';
|
component.userId = 'user-1';
|
||||||
|
|
||||||
component.name$?.subscribe(name => {
|
expect(userServiceSpy.getUserbyId$).toHaveBeenCalledWith('user-1');
|
||||||
expect(userServiceSpy.getUserbyId$).toHaveBeenCalledWith('user-1');
|
await expectAsync(firstValueFrom(component.name$!)).toBeResolvedTo('Benjamin');
|
||||||
expect(name).toBe('Benjamin');
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should map missing users to null names', done => {
|
it('should map missing users to null names', async () => {
|
||||||
userServiceSpy.getUserbyId$.and.returnValue(of(null));
|
userServiceSpy.getUserbyId$.and.returnValue(of(null));
|
||||||
|
|
||||||
component.userId = 'missing-user';
|
component.userId = 'missing-user';
|
||||||
|
|
||||||
component.name$?.subscribe(name => {
|
await expectAsync(firstValueFrom(component.name$!)).toBeResolvedTo(null);
|
||||||
expect(name).toBeNull();
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {TestBed} from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
import {of} from 'rxjs';
|
import {firstValueFrom, of} from 'rxjs';
|
||||||
import {UserService} from './user.service';
|
import {UserService} from './user.service';
|
||||||
import {UserSessionService} from './user-session.service';
|
import {UserSessionService} from './user-session.service';
|
||||||
import {UserSongUsageService} from './user-song-usage.service';
|
import {UserSongUsageService} from './user-song-usage.service';
|
||||||
@@ -54,17 +54,10 @@ describe('UserService', () => {
|
|||||||
expect(service).toBeTruthy();
|
expect(service).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should expose the session streams directly', done => {
|
it('should expose the session streams directly', async () => {
|
||||||
service.userId$.subscribe(userId => {
|
await expectAsync(firstValueFrom(service.userId$)).toBeResolvedTo('user-1');
|
||||||
expect(userId).toBe('user-1');
|
await expectAsync(firstValueFrom(service.user$)).toBeResolvedTo({id: 'user-1'} as never);
|
||||||
service.user$.subscribe(user => {
|
await expectAsync(firstValueFrom(service.users$)).toBeResolvedTo([{id: 'user-1'}] as never);
|
||||||
expect(user).toEqual({id: 'user-1'} as never);
|
|
||||||
service.users$.subscribe(users => {
|
|
||||||
expect(users).toEqual([{id: 'user-1'}] as never);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should delegate session operations to UserSessionService', async () => {
|
it('should delegate session operations to UserSessionService', async () => {
|
||||||
@@ -85,20 +78,13 @@ describe('UserService', () => {
|
|||||||
expect(sessionSpy.createNewUser).toHaveBeenCalledWith('mail', 'Benjamin', 'secret');
|
expect(sessionSpy.createNewUser).toHaveBeenCalledWith('mail', 'Benjamin', 'secret');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should delegate user lookup and loggedIn/list streams to UserSessionService', done => {
|
it('should delegate user lookup and loggedIn/list streams to UserSessionService', async () => {
|
||||||
service.getUserbyId$('user-2').subscribe(user => {
|
await expectAsync(firstValueFrom(service.getUserbyId$('user-2'))).toBeResolvedTo({id: 'user-2'} as never);
|
||||||
expect(user).toEqual({id: 'user-2'} as never);
|
await expectAsync(firstValueFrom(service.loggedIn$())).toBeResolvedTo(true);
|
||||||
service.loggedIn$().subscribe(loggedIn => {
|
await expectAsync(firstValueFrom(service.list$())).toBeResolvedTo([{id: 'user-1'}] as never);
|
||||||
expect(loggedIn).toBeTrue();
|
expect(sessionSpy.getUserbyId$).toHaveBeenCalledWith('user-2');
|
||||||
service.list$().subscribe(users => {
|
expect(sessionSpy.loggedIn$).toHaveBeenCalled();
|
||||||
expect(users).toEqual([{id: 'user-1'}] as never);
|
expect(sessionSpy.list$).toHaveBeenCalled();
|
||||||
expect(sessionSpy.getUserbyId$).toHaveBeenCalledWith('user-2');
|
|
||||||
expect(sessionSpy.loggedIn$).toHaveBeenCalled();
|
|
||||||
expect(sessionSpy.list$).toHaveBeenCalled();
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should delegate song usage operations to UserSongUsageService', async () => {
|
it('should delegate song usage operations to UserSongUsageService', async () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {AddSongComponent} from './add-song.component';
|
import {AddSongComponent} from './add-song.component';
|
||||||
|
|
||||||
@@ -6,11 +6,11 @@ describe('AddSongComponent', () => {
|
|||||||
let component: AddSongComponent;
|
let component: AddSongComponent;
|
||||||
let fixture: ComponentFixture<AddSongComponent>;
|
let fixture: ComponentFixture<AddSongComponent>;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [AddSongComponent],
|
imports: [AddSongComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(AddSongComponent);
|
fixture = TestBed.createComponent(AddSongComponent);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {BrandComponent} from './brand.component';
|
import {BrandComponent} from './brand.component';
|
||||||
|
|
||||||
@@ -6,11 +6,11 @@ describe('BrandComponent', () => {
|
|||||||
let component: BrandComponent;
|
let component: BrandComponent;
|
||||||
let fixture: ComponentFixture<BrandComponent>;
|
let fixture: ComponentFixture<BrandComponent>;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [BrandComponent],
|
imports: [BrandComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(BrandComponent);
|
fixture = TestBed.createComponent(BrandComponent);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {FilterComponent} from './filter.component';
|
import {FilterComponent} from './filter.component';
|
||||||
|
|
||||||
@@ -6,11 +6,11 @@ describe('FilterComponent', () => {
|
|||||||
let component: FilterComponent;
|
let component: FilterComponent;
|
||||||
let fixture: ComponentFixture<FilterComponent>;
|
let fixture: ComponentFixture<FilterComponent>;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [FilterComponent],
|
imports: [FilterComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(FilterComponent);
|
fixture = TestBed.createComponent(FilterComponent);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {LinkComponent} from './link.component';
|
import {LinkComponent} from './link.component';
|
||||||
|
|
||||||
@@ -6,11 +6,11 @@ describe('LinkComponent', () => {
|
|||||||
let component: LinkComponent;
|
let component: LinkComponent;
|
||||||
let fixture: ComponentFixture<LinkComponent>;
|
let fixture: ComponentFixture<LinkComponent>;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [LinkComponent],
|
imports: [LinkComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(LinkComponent);
|
fixture = TestBed.createComponent(LinkComponent);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {NavigationComponent} from './navigation.component';
|
import {NavigationComponent} from './navigation.component';
|
||||||
|
|
||||||
@@ -6,11 +6,11 @@ describe('NavigationComponent', () => {
|
|||||||
let component: NavigationComponent;
|
let component: NavigationComponent;
|
||||||
let fixture: ComponentFixture<NavigationComponent>;
|
let fixture: ComponentFixture<NavigationComponent>;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [NavigationComponent],
|
imports: [NavigationComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(NavigationComponent);
|
fixture = TestBed.createComponent(NavigationComponent);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {ButtonRowComponent} from './button-row.component';
|
import {ButtonRowComponent} from './button-row.component';
|
||||||
|
|
||||||
@@ -6,11 +6,11 @@ describe('ButtonRowComponent', () => {
|
|||||||
let component: ButtonRowComponent;
|
let component: ButtonRowComponent;
|
||||||
let fixture: ComponentFixture<ButtonRowComponent>;
|
let fixture: ComponentFixture<ButtonRowComponent>;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [ButtonRowComponent],
|
imports: [ButtonRowComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(ButtonRowComponent);
|
fixture = TestBed.createComponent(ButtonRowComponent);
|
||||||
|
|||||||
@@ -1,7 +1,21 @@
|
|||||||
|
:host {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host(.full-width) {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
transition: var(--transition);
|
transition: var(--transition);
|
||||||
|
|
||||||
|
:host(.full-width) & {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--primary-active);
|
color: var(--primary-active);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {ButtonComponent} from './button.component';
|
import {ButtonComponent} from './button.component';
|
||||||
|
|
||||||
@@ -6,11 +6,11 @@ describe('ButtonComponent', () => {
|
|||||||
let component: ButtonComponent;
|
let component: ButtonComponent;
|
||||||
let fixture: ComponentFixture<ButtonComponent>;
|
let fixture: ComponentFixture<ButtonComponent>;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [ButtonComponent],
|
imports: [ButtonComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(ButtonComponent);
|
fixture = TestBed.createComponent(ButtonComponent);
|
||||||
|
|||||||
@@ -9,8 +9,12 @@ import {FaIconComponent} from '@fortawesome/angular-fontawesome';
|
|||||||
templateUrl: './button.component.html',
|
templateUrl: './button.component.html',
|
||||||
styleUrls: ['./button.component.less'],
|
styleUrls: ['./button.component.less'],
|
||||||
imports: [MatButton, FaIconComponent],
|
imports: [MatButton, FaIconComponent],
|
||||||
|
host: {
|
||||||
|
'[class.full-width]': 'fullWidth',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
export class ButtonComponent {
|
export class ButtonComponent {
|
||||||
@Input() public disabled = false;
|
@Input() public disabled = false;
|
||||||
|
@Input() public fullWidth = false;
|
||||||
@Input() public icon: IconProp | null = null;
|
@Input() public icon: IconProp | null = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<div [class.fullscreen]="fullscreen" [class.padding]="padding" class="card">
|
<div @fade [class.fullscreen]="fullscreen" [class.padding]="padding" class="card">
|
||||||
@if (closeLink && !fullscreen) {
|
@if (closeLink && !fullscreen) {
|
||||||
<button [routerLink]="closeLink" class="btn-close" mat-icon-button>
|
<button [routerLink]="closeLink" class="btn-close" mat-icon-button>
|
||||||
<fa-icon [icon]="closeIcon"></fa-icon>
|
<fa-icon [icon]="closeIcon"></fa-icon>
|
||||||
|
|||||||
@@ -70,9 +70,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-close {
|
.btn-close {
|
||||||
|
--icon-button-color: var(--text-soft);
|
||||||
|
--icon-button-hover-color: var(--text);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
top: 15px;
|
top: 15px;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
color: var(--text-soft);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {CardComponent} from './card.component';
|
import {CardComponent} from './card.component';
|
||||||
|
|
||||||
@@ -6,11 +6,11 @@ describe('CardComponent', () => {
|
|||||||
let component: CardComponent;
|
let component: CardComponent;
|
||||||
let fixture: ComponentFixture<CardComponent>;
|
let fixture: ComponentFixture<CardComponent>;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [CardComponent],
|
imports: [CardComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(CardComponent);
|
fixture = TestBed.createComponent(CardComponent);
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ import {faTimes} from '@fortawesome/free-solid-svg-icons';
|
|||||||
import {MatIconButton} from '@angular/material/button';
|
import {MatIconButton} from '@angular/material/button';
|
||||||
import {RouterLink} from '@angular/router';
|
import {RouterLink} from '@angular/router';
|
||||||
import {FaIconComponent} from '@fortawesome/angular-fontawesome';
|
import {FaIconComponent} from '@fortawesome/angular-fontawesome';
|
||||||
|
import {fade} from '../../../animations';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-card',
|
selector: 'app-card',
|
||||||
templateUrl: './card.component.html',
|
templateUrl: './card.component.html',
|
||||||
styleUrls: ['./card.component.less'],
|
styleUrls: ['./card.component.less'],
|
||||||
imports: [MatIconButton, RouterLink, FaIconComponent],
|
imports: [MatIconButton, RouterLink, FaIconComponent],
|
||||||
|
animations: [fade],
|
||||||
})
|
})
|
||||||
export class CardComponent {
|
export class CardComponent {
|
||||||
@Input() public padding = true;
|
@Input() public padding = true;
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
<div class="header">
|
<div class="header">
|
||||||
|
@if (showFilterButton) {
|
||||||
<button (click)="onFilterClick()" [class.filter-active]="anyFilterActive" mat-icon-button>
|
<button (click)="onFilterClick()" [class.filter-active]="anyFilterActive" mat-icon-button>
|
||||||
<fa-icon [icon]="faFilter"></fa-icon>
|
<fa-icon [icon]="faFilter"></fa-icon>
|
||||||
</button>
|
</button>
|
||||||
|
}
|
||||||
<button mat-icon-button routerLink="new">
|
<button mat-icon-button routerLink="new">
|
||||||
<fa-icon [icon]="faNew"></fa-icon>
|
<fa-icon [icon]="faNew"></fa-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (filterVisible || anyFilterActive) {
|
@if (showFilterButton && (filterVisible || anyFilterActive)) {
|
||||||
<div @fade>
|
<div @fade>
|
||||||
<app-card>
|
<app-card>
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
|
|||||||
@@ -10,11 +10,15 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
color: var(--primary-hover);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-active {
|
.header .mat-mdc-icon-button {
|
||||||
color: var(--danger);
|
--icon-button-color: var(--primary-hover);
|
||||||
|
--icon-button-hover-color: var(--primary-active);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .mat-mdc-icon-button.filter-active {
|
||||||
|
--icon-button-color: var(--danger);
|
||||||
|
--icon-button-hover-color: var(--danger);
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {ListHeaderComponent} from './list-header.component';
|
import {ListHeaderComponent} from './list-header.component';
|
||||||
|
|
||||||
@@ -6,11 +6,11 @@ describe('ListHeaderComponent', () => {
|
|||||||
let component: ListHeaderComponent;
|
let component: ListHeaderComponent;
|
||||||
let fixture: ComponentFixture<ListHeaderComponent>;
|
let fixture: ComponentFixture<ListHeaderComponent>;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [ListHeaderComponent],
|
imports: [ListHeaderComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(ListHeaderComponent);
|
fixture = TestBed.createComponent(ListHeaderComponent);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export class ListHeaderComponent {
|
|||||||
public filterVisible = false;
|
public filterVisible = false;
|
||||||
@Output() public filterVisibleChanged = new EventEmitter<boolean>();
|
@Output() public filterVisibleChanged = new EventEmitter<boolean>();
|
||||||
@Input() public anyFilterActive = false;
|
@Input() public anyFilterActive = false;
|
||||||
|
@Input() public showFilterButton = true;
|
||||||
|
|
||||||
public onFilterClick(): void {
|
public onFilterClick(): void {
|
||||||
this.filterVisible = !this.filterVisible || this.anyFilterActive;
|
this.filterVisible = !this.filterVisible || this.anyFilterActive;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {LogoComponent} from './logo.component';
|
import {LogoComponent} from './logo.component';
|
||||||
|
|
||||||
@@ -6,11 +6,11 @@ describe('LogoComponent', () => {
|
|||||||
let component: LogoComponent;
|
let component: LogoComponent;
|
||||||
let fixture: ComponentFixture<LogoComponent>;
|
let fixture: ComponentFixture<LogoComponent>;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [LogoComponent],
|
imports: [LogoComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(LogoComponent);
|
fixture = TestBed.createComponent(LogoComponent);
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
button {
|
.mat-mdc-button {
|
||||||
|
--icon-button-color: var(--primary-color);
|
||||||
|
--icon-button-hover-color: var(--primary-active);
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: 0 var(--button-padding, 5px);
|
padding: 0 var(--button-padding, 5px);
|
||||||
font-size: var(--button-font-size, 1em);
|
font-size: var(--button-font-size, 1em);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {MenuButtonComponent} from './menu-button.component';
|
import {MenuButtonComponent} from './menu-button.component';
|
||||||
|
|
||||||
@@ -6,11 +6,11 @@ describe('MenuButtonComponent', () => {
|
|||||||
let component: MenuButtonComponent;
|
let component: MenuButtonComponent;
|
||||||
let fixture: ComponentFixture<MenuButtonComponent>;
|
let fixture: ComponentFixture<MenuButtonComponent>;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [MenuButtonComponent],
|
imports: [MenuButtonComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(MenuButtonComponent);
|
fixture = TestBed.createComponent(MenuButtonComponent);
|
||||||
|
|||||||
@@ -1 +1,12 @@
|
|||||||
<aside></aside>
|
<button class="sidebar-toggle" (click)="toggle()" [attr.aria-expanded]="!collapsed" aria-label="Sidebar umschalten" mat-icon-button type="button">
|
||||||
|
<fa-icon [icon]="collapsed ? closedIcon : openIcon"></fa-icon>
|
||||||
|
</button>
|
||||||
|
<aside [class.collapsed]="collapsed">
|
||||||
|
<div class="sidebar-toggle-placeholder" aria-hidden="true"></div>
|
||||||
|
<div class="sidebar-body">
|
||||||
|
<ng-content select="[sidebar]"></ng-content>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
<div class="content">
|
||||||
|
<ng-content select="[content]"></ng-content>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,48 @@
|
|||||||
|
:host {
|
||||||
|
--sidebar-width: 300px;
|
||||||
|
--sidebar-toggle-size: 48px;
|
||||||
|
--sidebar-toggle-offset: 12px;
|
||||||
|
display: grid;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
grid-template-columns: var(--sidebar-width) minmax(0, 1fr);
|
||||||
|
align-items: start;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: grid-template-columns 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host.collapsed {
|
||||||
|
grid-template-columns: 0 minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-toggle {
|
||||||
|
--icon-button-color: var(--primary-hover);
|
||||||
|
--icon-button-hover-color: var(--primary-active);
|
||||||
|
position: fixed;
|
||||||
|
top: calc(50px + var(--sidebar-toggle-offset));
|
||||||
|
left: var(--sidebar-toggle-offset);
|
||||||
|
z-index: 11;
|
||||||
|
color: var(--icon-button-color);
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host.collapsed .sidebar-toggle {
|
||||||
|
--icon-button-color: var(--text-inverse);
|
||||||
|
--icon-button-hover-color: var(--text-inverse);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-toggle fa-icon {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-toggle:hover {
|
||||||
|
color: var(--icon-button-hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
aside {
|
aside {
|
||||||
width: 200px;
|
width: var(--sidebar-width);
|
||||||
height: calc(100vh - 50px);
|
height: calc(100vh - 50px);
|
||||||
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -8,4 +51,28 @@ aside {
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
|
box-shadow: var(--shadow-card-2);
|
||||||
|
overflow: hidden;
|
||||||
|
transform: translateX(0);
|
||||||
|
transition: transform 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
aside.collapsed {
|
||||||
|
transform: translateX(calc(-1 * var(--sidebar-width)));
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-toggle-placeholder {
|
||||||
|
height: calc(var(--sidebar-toggle-size) + var(--sidebar-toggle-offset));
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-body {
|
||||||
|
height: calc(100% - var(--sidebar-toggle-size) - var(--sidebar-toggle-offset));
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
grid-column: 2;
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,23 @@
|
|||||||
import {Component} from '@angular/core';
|
import {Component} from '@angular/core';
|
||||||
|
import {MatIconButton} from '@angular/material/button';
|
||||||
|
import {FaIconComponent} from '@fortawesome/angular-fontawesome';
|
||||||
|
import {faBars, faChevronLeft} from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-sidebar',
|
selector: 'app-sidebar',
|
||||||
imports: [],
|
imports: [MatIconButton, FaIconComponent],
|
||||||
templateUrl: './sidebar.component.html',
|
templateUrl: './sidebar.component.html',
|
||||||
styleUrl: './sidebar.component.less',
|
styleUrl: './sidebar.component.less',
|
||||||
|
host: {
|
||||||
|
'[class.collapsed]': 'collapsed',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
export class SidebarComponent {}
|
export class SidebarComponent {
|
||||||
|
public collapsed = true;
|
||||||
|
public openIcon = faChevronLeft;
|
||||||
|
public closedIcon = faBars;
|
||||||
|
|
||||||
|
public toggle(): void {
|
||||||
|
this.collapsed = !this.collapsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
import {SongTextComponent} from './song-text.component';
|
import {SongTextComponent} from './song-text.component';
|
||||||
|
|
||||||
@@ -6,11 +6,11 @@ describe('SongTextComponent', () => {
|
|||||||
let component: SongTextComponent;
|
let component: SongTextComponent;
|
||||||
let fixture: ComponentFixture<SongTextComponent>;
|
let fixture: ComponentFixture<SongTextComponent>;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(async () => {
|
||||||
void TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [SongTextComponent],
|
imports: [SongTextComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(SongTextComponent);
|
fixture = TestBed.createComponent(SongTextComponent);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {TestBed} from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
import {Router} from '@angular/router';
|
import {Router} from '@angular/router';
|
||||||
import {of} from 'rxjs';
|
import {firstValueFrom, of} from 'rxjs';
|
||||||
import {UserService} from '../../services/user/user.service';
|
import {UserService} from '../../services/user/user.service';
|
||||||
import {RoleGuard} from './role.guard';
|
import {RoleGuard} from './role.guard';
|
||||||
|
|
||||||
@@ -30,14 +30,13 @@ describe('RoleGuard', () => {
|
|||||||
expect(() => guard.canActivate({data: {}} as never)).toThrowError('requiredRoles is not defined!');
|
expect(() => guard.canActivate({data: {}} as never)).toThrowError('requiredRoles is not defined!');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should deny access when there is no current user', done => {
|
it('should deny access when there is no current user', async () => {
|
||||||
guard.canActivate({data: {requiredRoles: ['leader']}} as never).subscribe(result => {
|
await expectAsync(firstValueFrom(guard.canActivate({data: {requiredRoles: ['leader']}} as never))).toBeResolvedTo(
|
||||||
expect(result).toEqual({commands: ['brand', 'new-user']} as never);
|
{commands: ['brand', 'new-user']} as never
|
||||||
done();
|
);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow admins regardless of requiredRoles', done => {
|
it('should allow admins regardless of requiredRoles', async () => {
|
||||||
TestBed.resetTestingModule();
|
TestBed.resetTestingModule();
|
||||||
routerSpy = jasmine.createSpyObj<Router>('Router', ['createUrlTree']);
|
routerSpy = jasmine.createSpyObj<Router>('Router', ['createUrlTree']);
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@@ -48,13 +47,10 @@ describe('RoleGuard', () => {
|
|||||||
});
|
});
|
||||||
guard = TestBed.inject(RoleGuard);
|
guard = TestBed.inject(RoleGuard);
|
||||||
|
|
||||||
guard.canActivate({data: {requiredRoles: ['leader']}} as never).subscribe(result => {
|
await expectAsync(firstValueFrom(guard.canActivate({data: {requiredRoles: ['leader']}} as never))).toBeResolvedTo(true);
|
||||||
expect(result).toBeTrue();
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow users with a matching required role', done => {
|
it('should allow users with a matching required role', async () => {
|
||||||
TestBed.resetTestingModule();
|
TestBed.resetTestingModule();
|
||||||
routerSpy = jasmine.createSpyObj<Router>('Router', ['createUrlTree']);
|
routerSpy = jasmine.createSpyObj<Router>('Router', ['createUrlTree']);
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@@ -65,13 +61,10 @@ describe('RoleGuard', () => {
|
|||||||
});
|
});
|
||||||
guard = TestBed.inject(RoleGuard);
|
guard = TestBed.inject(RoleGuard);
|
||||||
|
|
||||||
guard.canActivate({data: {requiredRoles: ['leader']}} as never).subscribe(result => {
|
await expectAsync(firstValueFrom(guard.canActivate({data: {requiredRoles: ['leader']}} as never))).toBeResolvedTo(true);
|
||||||
expect(result).toBeTrue();
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should redirect users without the required role to their role default route', done => {
|
it('should redirect users without the required role to their role default route', async () => {
|
||||||
TestBed.resetTestingModule();
|
TestBed.resetTestingModule();
|
||||||
routerSpy = jasmine.createSpyObj<Router>('Router', ['createUrlTree']);
|
routerSpy = jasmine.createSpyObj<Router>('Router', ['createUrlTree']);
|
||||||
routerSpy.createUrlTree.and.returnValue({redirect: ['presentation']} as never);
|
routerSpy.createUrlTree.and.returnValue({redirect: ['presentation']} as never);
|
||||||
@@ -83,14 +76,12 @@ describe('RoleGuard', () => {
|
|||||||
});
|
});
|
||||||
guard = TestBed.inject(RoleGuard);
|
guard = TestBed.inject(RoleGuard);
|
||||||
|
|
||||||
guard.canActivate({data: {requiredRoles: ['leader']}} as never).subscribe(result => {
|
const result = await firstValueFrom(guard.canActivate({data: {requiredRoles: ['leader']}} as never));
|
||||||
expect(routerSpy.createUrlTree).toHaveBeenCalledWith(['presentation']);
|
expect(routerSpy.createUrlTree).toHaveBeenCalledWith(['presentation']);
|
||||||
expect(result).toEqual({redirect: ['presentation']} as never);
|
expect(result).toEqual({redirect: ['presentation']} as never);
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should redirect members to shows instead of new-user', done => {
|
it('should redirect members to shows instead of new-user', async () => {
|
||||||
TestBed.resetTestingModule();
|
TestBed.resetTestingModule();
|
||||||
routerSpy = jasmine.createSpyObj<Router>('Router', ['createUrlTree']);
|
routerSpy = jasmine.createSpyObj<Router>('Router', ['createUrlTree']);
|
||||||
routerSpy.createUrlTree.and.returnValue({redirect: ['shows']} as never);
|
routerSpy.createUrlTree.and.returnValue({redirect: ['shows']} as never);
|
||||||
@@ -102,14 +93,12 @@ describe('RoleGuard', () => {
|
|||||||
});
|
});
|
||||||
guard = TestBed.inject(RoleGuard);
|
guard = TestBed.inject(RoleGuard);
|
||||||
|
|
||||||
guard.canActivate({data: {requiredRoles: ['user']}} as never).subscribe(result => {
|
const result = await firstValueFrom(guard.canActivate({data: {requiredRoles: ['user']}} as never));
|
||||||
expect(routerSpy.createUrlTree).toHaveBeenCalledWith(['shows']);
|
expect(routerSpy.createUrlTree).toHaveBeenCalledWith(['shows']);
|
||||||
expect(result).toEqual({redirect: ['shows']} as never);
|
expect(result).toEqual({redirect: ['shows']} as never);
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should choose a matching default route from all assigned roles', done => {
|
it('should choose a matching default route from all assigned roles', async () => {
|
||||||
TestBed.resetTestingModule();
|
TestBed.resetTestingModule();
|
||||||
routerSpy = jasmine.createSpyObj<Router>('Router', ['createUrlTree']);
|
routerSpy = jasmine.createSpyObj<Router>('Router', ['createUrlTree']);
|
||||||
routerSpy.createUrlTree.and.callFake(commands => ({redirect: commands}) as never);
|
routerSpy.createUrlTree.and.callFake(commands => ({redirect: commands}) as never);
|
||||||
@@ -121,10 +110,8 @@ describe('RoleGuard', () => {
|
|||||||
});
|
});
|
||||||
guard = TestBed.inject(RoleGuard);
|
guard = TestBed.inject(RoleGuard);
|
||||||
|
|
||||||
guard.canActivate({data: {requiredRoles: ['presenter']}} as never).subscribe(result => {
|
const result = await firstValueFrom(guard.canActivate({data: {requiredRoles: ['presenter']}} as never));
|
||||||
expect(routerSpy.createUrlTree).toHaveBeenCalledWith(['shows']);
|
expect(routerSpy.createUrlTree).toHaveBeenCalledWith(['shows']);
|
||||||
expect(result).toEqual({redirect: ['shows']} as never);
|
expect(result).toEqual({redirect: ['shows']} as never);
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ $wgenerator-theme: mat.m2-define-light-theme((
|
|||||||
warn: $wgenerator-warn,
|
warn: $wgenerator-warn,
|
||||||
),
|
),
|
||||||
typography: mat.m2-define-typography-config(),
|
typography: mat.m2-define-typography-config(),
|
||||||
density: 0,
|
density: -2,
|
||||||
));
|
));
|
||||||
|
|
||||||
@include mat.all-component-themes($wgenerator-theme);
|
@include mat.all-component-themes($wgenerator-theme);
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
z-index: 1;
|
z-index: 100;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,10 @@
|
|||||||
--focus-ring: 0 0 0 2px rgba(111, 143, 149, 0.28);
|
--focus-ring: 0 0 0 2px rgba(111, 143, 149, 0.28);
|
||||||
--transition: all 300ms ease-in-out;
|
--transition: all 300ms ease-in-out;
|
||||||
--transition-fast: all 150ms ease-in-out;
|
--transition-fast: all 150ms ease-in-out;
|
||||||
|
--icon-button-color: var(--primary-color);
|
||||||
|
--icon-button-hover-color: var(--primary-active);
|
||||||
|
--icon-button-opacity: 1;
|
||||||
|
--icon-button-hover-opacity: 1;
|
||||||
|
|
||||||
|
|
||||||
--mat-dialog-supporting-text-color: var(--text);
|
--mat-dialog-supporting-text-color: var(--text);
|
||||||
@@ -78,11 +82,11 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mat-mdc-icon-button {
|
.mat-mdc-icon-button {
|
||||||
color: var(--primary-color) !important;
|
color: var(--icon-button-color, var(--primary-color));
|
||||||
transition: var(--transition);
|
transition: var(--transition);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--primary-active) !important;
|
color: var(--icon-button-hover-color, var(--primary-active));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,11 +107,11 @@ body .cdk-overlay-transparent-backdrop.cdk-overlay-backdrop-showing {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-icon {
|
.btn-icon {
|
||||||
opacity: 0.2;
|
opacity: var(--icon-button-opacity);
|
||||||
transition: var(--transition);
|
transition: var(--transition);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
opacity: 1;
|
opacity: var(--icon-button-hover-opacity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,6 +130,5 @@ body {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
--mat-form-field-container-text-line-height: 16px;
|
|
||||||
--mat-form-field-container-text-size: 16px;
|
|
||||||
}
|
}
|
||||||
|
|||||||
228
src/test-vitest.ts
Normal file
228
src/test-vitest.ts
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
import {expect, vi} from 'vitest';
|
||||||
|
|
||||||
|
import 'zone.js/testing';
|
||||||
|
import {TestBed} from '@angular/core/testing';
|
||||||
|
import {provideNoopAnimations} from '@angular/platform-browser/animations';
|
||||||
|
import {ActivatedRoute, provideRouter} from '@angular/router';
|
||||||
|
import {BehaviorSubject, Observable, of} from 'rxjs';
|
||||||
|
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
|
||||||
|
import {provideNativeDateAdapter} from '@angular/material/core';
|
||||||
|
import {getApp, getApps, initializeApp, provideFirebaseApp} from '@angular/fire/app';
|
||||||
|
import {getAuth, provideAuth} from '@angular/fire/auth';
|
||||||
|
import {initializeFirestore, provideFirestore} from '@angular/fire/firestore';
|
||||||
|
import {getStorage, provideStorage} from '@angular/fire/storage';
|
||||||
|
import {environment} from './environments/environment';
|
||||||
|
import {DbService} from './app/services/db.service';
|
||||||
|
|
||||||
|
type TestingModuleDefinition = Parameters<typeof TestBed.configureTestingModule>[0];
|
||||||
|
type TestingProviderList = NonNullable<NonNullable<TestingModuleDefinition>['providers']>;
|
||||||
|
type CollectionStub = {
|
||||||
|
valueChanges: () => Observable<unknown[]>;
|
||||||
|
add: () => Promise<{id: string}>;
|
||||||
|
};
|
||||||
|
type DocumentStub = {
|
||||||
|
set: () => Promise<void>;
|
||||||
|
update: () => Promise<void>;
|
||||||
|
delete: () => Promise<void>;
|
||||||
|
collection: () => CollectionStub;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MockFunction = ReturnType<typeof vi.fn> & {
|
||||||
|
and: {
|
||||||
|
returnValue: (value: unknown) => MockFunction;
|
||||||
|
resolveTo: (value: unknown) => MockFunction;
|
||||||
|
rejectWith: (value: unknown) => MockFunction;
|
||||||
|
callFake: (fn: (...args: unknown[]) => unknown) => MockFunction;
|
||||||
|
callThrough: () => MockFunction;
|
||||||
|
};
|
||||||
|
calls: {
|
||||||
|
argsFor: (index: number) => unknown[];
|
||||||
|
mostRecent: () => {args: unknown[]};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const routeParams$ = new BehaviorSubject<Record<string, unknown>>({});
|
||||||
|
const queryParams$ = new BehaviorSubject<Record<string, unknown>>({});
|
||||||
|
const defaultFirebaseApp = getApps().length > 0 ? getApp() : initializeApp(environment.firebase);
|
||||||
|
|
||||||
|
const defaultTestingProviders: TestingProviderList = [
|
||||||
|
provideNoopAnimations(),
|
||||||
|
provideNativeDateAdapter(),
|
||||||
|
provideRouter([]),
|
||||||
|
provideFirebaseApp(() => defaultFirebaseApp),
|
||||||
|
provideAuth(() => getAuth(defaultFirebaseApp)),
|
||||||
|
provideFirestore(() => initializeFirestore(defaultFirebaseApp, {})),
|
||||||
|
provideStorage(() => getStorage(defaultFirebaseApp)),
|
||||||
|
{
|
||||||
|
provide: ActivatedRoute,
|
||||||
|
useValue: {
|
||||||
|
snapshot: {params: {}, queryParams: {}, data: {}},
|
||||||
|
params: routeParams$.asObservable(),
|
||||||
|
queryParams: queryParams$.asObservable(),
|
||||||
|
data: of({}),
|
||||||
|
fragment: of(null),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{provide: MAT_DIALOG_DATA, useValue: {}},
|
||||||
|
{provide: MatDialogRef, useValue: {close: () => void 0}},
|
||||||
|
{
|
||||||
|
provide: DbService,
|
||||||
|
useValue: {
|
||||||
|
col$: () => of([]),
|
||||||
|
doc$: () => of(null),
|
||||||
|
col: (): CollectionStub => ({
|
||||||
|
valueChanges: () => of([]),
|
||||||
|
add: () => Promise.resolve({id: 'test-id'}),
|
||||||
|
}),
|
||||||
|
doc: (): DocumentStub => ({
|
||||||
|
set: () => Promise.resolve(),
|
||||||
|
update: () => Promise.resolve(),
|
||||||
|
delete: () => Promise.resolve(),
|
||||||
|
collection: (): CollectionStub => ({
|
||||||
|
valueChanges: () => of([]),
|
||||||
|
add: () => Promise.resolve({id: 'test-id'}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const originalConfigureTestingModule = TestBed.configureTestingModule.bind(TestBed);
|
||||||
|
const configureTestingModule: typeof TestBed.configureTestingModule = moduleDef => {
|
||||||
|
const extraProviders: TestingProviderList = moduleDef?.providers ?? [];
|
||||||
|
const mergedModuleDef: TestingModuleDefinition = moduleDef ? {...moduleDef} : {};
|
||||||
|
mergedModuleDef.providers = defaultTestingProviders.concat(extraProviders);
|
||||||
|
|
||||||
|
return originalConfigureTestingModule(mergedModuleDef);
|
||||||
|
};
|
||||||
|
TestBed.configureTestingModule = configureTestingModule;
|
||||||
|
|
||||||
|
function decorateMock<T extends ReturnType<typeof vi.fn>>(mock: T): T & MockFunction {
|
||||||
|
const decorated = mock as T & MockFunction;
|
||||||
|
|
||||||
|
Object.defineProperty(decorated, 'and', {
|
||||||
|
configurable: true,
|
||||||
|
get: () => ({
|
||||||
|
returnValue(value: unknown) {
|
||||||
|
decorated.mockReturnValue(value);
|
||||||
|
return decorated;
|
||||||
|
},
|
||||||
|
resolveTo(value: unknown) {
|
||||||
|
decorated.mockResolvedValue(value);
|
||||||
|
return decorated;
|
||||||
|
},
|
||||||
|
rejectWith(value: unknown) {
|
||||||
|
decorated.mockRejectedValue(value);
|
||||||
|
return decorated;
|
||||||
|
},
|
||||||
|
callFake(fn: (...args: unknown[]) => unknown) {
|
||||||
|
decorated.mockImplementation(fn);
|
||||||
|
return decorated;
|
||||||
|
},
|
||||||
|
callThrough() {
|
||||||
|
return decorated;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.defineProperty(decorated, 'calls', {
|
||||||
|
configurable: true,
|
||||||
|
get: () => ({
|
||||||
|
argsFor(index: number) {
|
||||||
|
return decorated.mock.calls[index] ?? [];
|
||||||
|
},
|
||||||
|
mostRecent() {
|
||||||
|
const args = decorated.mock.lastCall ?? [];
|
||||||
|
return {args};
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
return decorated;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSpy(name?: string): MockFunction {
|
||||||
|
const spy = decorateMock(vi.fn());
|
||||||
|
if (name) {
|
||||||
|
spy.mockName(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return spy;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
for (const methodName of methods) {
|
||||||
|
result[methodName] = createSpy(`${baseName}.${methodName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(methodNames)) {
|
||||||
|
for (const [key, value] of Object.entries(methodNames)) {
|
||||||
|
(result[key] as MockFunction).and.returnValue(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (propertyValues) {
|
||||||
|
for (const [key, value] of Object.entries(propertyValues)) {
|
||||||
|
result[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
function spyOnCompat<T extends object, K extends keyof T>(object: T, methodName: K): MockFunction {
|
||||||
|
const spy = vi.spyOn(object as Record<PropertyKey, (...args: unknown[]) => unknown>, methodName as PropertyKey);
|
||||||
|
return decorateMock(spy as unknown as ReturnType<typeof vi.fn>);
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectAsyncCompat<T>(value: Promise<T>) {
|
||||||
|
return {
|
||||||
|
async toBeResolvedTo(expected: T) {
|
||||||
|
await expect(value).resolves.toEqual(expected);
|
||||||
|
},
|
||||||
|
async toBeRejectedWithError(expected?: string | RegExp | Error) {
|
||||||
|
if (expected instanceof Error) {
|
||||||
|
await expect(value).rejects.toThrowError(expected.message);
|
||||||
|
} else if (expected !== undefined) {
|
||||||
|
await expect(value).rejects.toThrowError(expected);
|
||||||
|
} else {
|
||||||
|
await expect(value).rejects.toThrowError();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
expect.extend({
|
||||||
|
toBeTrue(received: unknown) {
|
||||||
|
return {
|
||||||
|
pass: received === true,
|
||||||
|
message: () => `expected ${String(received)} to be true`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
toBeFalse(received: unknown) {
|
||||||
|
return {
|
||||||
|
pass: received === false,
|
||||||
|
message: () => `expected ${String(received)} to be false`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.assign(globalThis, {
|
||||||
|
spyOn: spyOnCompat,
|
||||||
|
expectAsync: expectAsyncCompat,
|
||||||
|
jasmine: {
|
||||||
|
createSpy,
|
||||||
|
createSpyObj,
|
||||||
|
any: expect.any,
|
||||||
|
anything: expect.anything,
|
||||||
|
objectContaining: expect.objectContaining,
|
||||||
|
stringMatching: expect.stringMatching,
|
||||||
|
},
|
||||||
|
});
|
||||||
90
src/test.ts
90
src/test.ts
@@ -1,90 +0,0 @@
|
|||||||
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
|
|
||||||
|
|
||||||
import 'zone.js/testing';
|
|
||||||
import {getTestBed, TestBed} from '@angular/core/testing';
|
|
||||||
import {BrowserDynamicTestingModule, platformBrowserDynamicTesting} from '@angular/platform-browser-dynamic/testing';
|
|
||||||
import {provideNoopAnimations} from '@angular/platform-browser/animations';
|
|
||||||
import {ActivatedRoute, provideRouter} from '@angular/router';
|
|
||||||
import {BehaviorSubject, of} from 'rxjs';
|
|
||||||
import {Observable} from 'rxjs';
|
|
||||||
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
|
|
||||||
import {provideNativeDateAdapter} from '@angular/material/core';
|
|
||||||
import {provideFirebaseApp, initializeApp} from '@angular/fire/app';
|
|
||||||
import {getApp, getApps} from '@angular/fire/app';
|
|
||||||
import {getAuth, provideAuth} from '@angular/fire/auth';
|
|
||||||
import {initializeFirestore, provideFirestore} from '@angular/fire/firestore';
|
|
||||||
import {getStorage, provideStorage} from '@angular/fire/storage';
|
|
||||||
import {environment} from './environments/environment';
|
|
||||||
import {DbService} from './app/services/db.service';
|
|
||||||
|
|
||||||
type req = {keys: () => {map: (context: req) => void}};
|
|
||||||
type TestingModuleDefinition = Parameters<typeof TestBed.configureTestingModule>[0];
|
|
||||||
type TestingProviderList = NonNullable<NonNullable<TestingModuleDefinition>['providers']>;
|
|
||||||
type CollectionStub = {
|
|
||||||
valueChanges: () => Observable<unknown[]>;
|
|
||||||
add: () => Promise<{id: string}>;
|
|
||||||
};
|
|
||||||
type DocumentStub = {
|
|
||||||
set: () => Promise<void>;
|
|
||||||
update: () => Promise<void>;
|
|
||||||
delete: () => Promise<void>;
|
|
||||||
collection: () => CollectionStub;
|
|
||||||
};
|
|
||||||
|
|
||||||
// First, initialize the Angular testing environment.
|
|
||||||
getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
|
|
||||||
|
|
||||||
const routeParams$ = new BehaviorSubject<Record<string, unknown>>({});
|
|
||||||
const queryParams$ = new BehaviorSubject<Record<string, unknown>>({});
|
|
||||||
|
|
||||||
const defaultTestingProviders: TestingProviderList = [
|
|
||||||
provideNoopAnimations(),
|
|
||||||
provideNativeDateAdapter(),
|
|
||||||
provideRouter([]),
|
|
||||||
provideFirebaseApp(() => (getApps().some(app => app.name === 'wgenerator-tests') ? getApp('wgenerator-tests') : initializeApp(environment.firebase, 'wgenerator-tests'))),
|
|
||||||
provideAuth(() => getAuth(getApp('wgenerator-tests'))),
|
|
||||||
provideFirestore(() => initializeFirestore(getApp('wgenerator-tests'), {})),
|
|
||||||
provideStorage(() => getStorage(getApp('wgenerator-tests'))),
|
|
||||||
{
|
|
||||||
provide: ActivatedRoute,
|
|
||||||
useValue: {
|
|
||||||
snapshot: {params: {}, queryParams: {}, data: {}},
|
|
||||||
params: routeParams$.asObservable(),
|
|
||||||
queryParams: queryParams$.asObservable(),
|
|
||||||
data: of({}),
|
|
||||||
fragment: of(null),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{provide: MAT_DIALOG_DATA, useValue: {}},
|
|
||||||
{provide: MatDialogRef, useValue: {close: () => void 0}},
|
|
||||||
{
|
|
||||||
provide: DbService,
|
|
||||||
useValue: {
|
|
||||||
col$: () => of([]),
|
|
||||||
doc$: () => of(null),
|
|
||||||
col: (): CollectionStub => ({
|
|
||||||
valueChanges: () => of([]),
|
|
||||||
add: () => Promise.resolve({id: 'test-id'}),
|
|
||||||
}),
|
|
||||||
doc: (): DocumentStub => ({
|
|
||||||
set: () => Promise.resolve(),
|
|
||||||
update: () => Promise.resolve(),
|
|
||||||
delete: () => Promise.resolve(),
|
|
||||||
collection: (): CollectionStub => ({
|
|
||||||
valueChanges: () => of([]),
|
|
||||||
add: () => Promise.resolve({id: 'test-id'}),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const originalConfigureTestingModule = TestBed.configureTestingModule.bind(TestBed);
|
|
||||||
const configureTestingModule: typeof TestBed.configureTestingModule = moduleDef => {
|
|
||||||
const extraProviders: TestingProviderList = moduleDef?.providers ?? [];
|
|
||||||
const mergedModuleDef: TestingModuleDefinition = moduleDef ? {...moduleDef} : {};
|
|
||||||
mergedModuleDef.providers = defaultTestingProviders.concat(extraProviders);
|
|
||||||
|
|
||||||
return originalConfigureTestingModule(mergedModuleDef);
|
|
||||||
};
|
|
||||||
TestBed.configureTestingModule = configureTestingModule;
|
|
||||||
44
src/types/jasmine-compat.d.ts
vendored
Normal file
44
src/types/jasmine-compat.d.ts
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
type SpyAnd = {
|
||||||
|
returnValue(value?: unknown): jasmine.Spy;
|
||||||
|
resolveTo(value?: unknown): jasmine.Spy;
|
||||||
|
rejectWith(value?: unknown): jasmine.Spy;
|
||||||
|
callFake(fn: (...args: any[]) => unknown): jasmine.Spy;
|
||||||
|
callThrough(): jasmine.Spy;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SpyCalls = {
|
||||||
|
argsFor(index: number): any[];
|
||||||
|
mostRecent(): {args: any[]};
|
||||||
|
};
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
function spyOn<T = any>(object: T, methodName: any): 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 SpyObj<T> = {
|
||||||
|
[K in keyof T]: T[K] extends (...args: any[]) => any ? 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 any(expectedClass: unknown): unknown;
|
||||||
|
function anything(): unknown;
|
||||||
|
function objectContaining<T>(value: Partial<T>): unknown;
|
||||||
|
function stringMatching(value: string | RegExp): unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
1
src/types/vitest-globals.d.ts
vendored
Normal file
1
src/types/vitest-globals.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vitest/globals" />
|
||||||
13
src/types/vitest-matchers.d.ts
vendored
Normal file
13
src/types/vitest-matchers.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import 'vitest';
|
||||||
|
|
||||||
|
declare module 'vitest' {
|
||||||
|
interface Assertion<T = any> {
|
||||||
|
toBeTrue(): T;
|
||||||
|
toBeFalse(): T;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AsymmetricMatchersContaining {
|
||||||
|
toBeTrue(): void;
|
||||||
|
toBeFalse(): void;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,15 +3,12 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "./out-tsc/spec",
|
"outDir": "./out-tsc/spec",
|
||||||
"types": [
|
"types": [
|
||||||
"jasmine",
|
|
||||||
"node"
|
"node"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"files": [
|
|
||||||
"src/test.ts",
|
|
||||||
"src/polyfills.ts"
|
|
||||||
],
|
|
||||||
"include": [
|
"include": [
|
||||||
|
"src/test-vitest.ts",
|
||||||
|
"src/polyfills.ts",
|
||||||
"src/**/*.spec.ts",
|
"src/**/*.spec.ts",
|
||||||
"src/**/*.d.ts"
|
"src/**/*.d.ts"
|
||||||
]
|
]
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user