Auto-formatted every file to keep everything consistent.

This commit is contained in:
Tom
2025-03-18 14:03:07 +00:00
parent 74b282ccfd
commit 9201f9b6c5
91 changed files with 14891 additions and 14767 deletions

View File

@ -1,124 +1,124 @@
{ {
"$schema": "./node_modules/@angular/cli/lib/config/schema.json", "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1, "version": 1,
"newProjectRoot": "projects", "newProjectRoot": "projects",
"projects": { "projects": {
"hermes-web-angular": { "hermes-web-angular": {
"projectType": "application", "projectType": "application",
"schematics": { "schematics": {
"@schematics/angular:component": { "@schematics/angular:component": {
"style": "scss" "style": "scss"
} }
}, },
"root": "", "root": "",
"sourceRoot": "src", "sourceRoot": "src",
"prefix": "app", "prefix": "app",
"architect": { "architect": {
"build": { "build": {
"builder": "@angular-devkit/build-angular:application", "builder": "@angular-devkit/build-angular:application",
"options": { "options": {
"outputPath": "dist/hermes-web-angular", "outputPath": "dist/hermes-web-angular",
"index": "src/index.html", "index": "src/index.html",
"browser": "src/main.ts", "browser": "src/main.ts",
"polyfills": [ "polyfills": [
"zone.js" "zone.js"
], ],
"tsConfig": "tsconfig.app.json", "tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss", "inlineStyleLanguage": "scss",
"assets": [ "assets": [
{ {
"glob": "**/*", "glob": "**/*",
"input": "public" "input": "public"
} }
], ],
"styles": [ "styles": [
"@angular/material/prebuilt-themes/azure-blue.css", "@angular/material/prebuilt-themes/azure-blue.css",
"src/styles.scss" "src/styles.scss"
], ],
"scripts": [], "scripts": [],
"server": "src/main.server.ts", "server": "src/main.server.ts",
"prerender": true, "prerender": true,
"ssr": { "ssr": {
"entry": "server.ts" "entry": "server.ts"
} }
}, },
"configurations": { "configurations": {
"production": { "production": {
"budgets": [ "budgets": [
{ {
"type": "initial", "type": "initial",
"maximumWarning": "1024kB", "maximumWarning": "1024kB",
"maximumError": "1MB" "maximumError": "1MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kB",
"maximumError": "4kB"
}
],
"optimization": true,
"sourceMap": false,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"index": {
"input": "src/index.prod.html",
"output": "index.html"
},
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true,
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.development.ts"
}
]
}
},
"defaultConfiguration": "production"
}, },
{ "serve": {
"type": "anyComponentStyle", "builder": "@angular-devkit/build-angular:dev-server",
"maximumWarning": "2kB", "configurations": {
"maximumError": "4kB" "production": {
"buildTarget": "hermes-web-angular:build:production"
},
"development": {
"buildTarget": "hermes-web-angular:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"@angular/material/prebuilt-themes/azure-blue.css",
"src/styles.scss"
],
"scripts": []
}
} }
],
"optimization": true,
"sourceMap": false,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"index": {
"input": "src/index.prod.html",
"output": "index.html"
},
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true,
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.development.ts"
}
]
} }
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "hermes-web-angular:build:production"
},
"development": {
"buildTarget": "hermes-web-angular:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"@angular/material/prebuilt-themes/azure-blue.css",
"src/styles.scss"
],
"scripts": []
}
} }
}
} }
}
} }

28210
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,50 +1,50 @@
{ {
"name": "hermes-web-angular", "name": "hermes-web-angular",
"version": "0.0.0", "version": "0.0.0",
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",
"start": "ng serve -c production --host 0.0.0.0 --watch false", "start": "ng serve -c production --host 0.0.0.0 --watch false",
"build": "ng build", "build": "ng build",
"watch": "ng serve -c development --host 0.0.0.0", "watch": "ng serve -c development --host 0.0.0.0",
"test": "ng test", "test": "ng test",
"serve:ssr:hermes-web-angular": "node dist/hermes-web-angular/server/server.mjs" "serve:ssr:hermes-web-angular": "node dist/hermes-web-angular/server/server.mjs"
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^19.0.5", "@angular/animations": "^19.0.5",
"@angular/cdk": "^19.0.4", "@angular/cdk": "^19.0.4",
"@angular/common": "^19.0.5", "@angular/common": "^19.0.5",
"@angular/compiler": "^19.0.5", "@angular/compiler": "^19.0.5",
"@angular/core": "^19.0.5", "@angular/core": "^19.0.5",
"@angular/forms": "^19.0.5", "@angular/forms": "^19.0.5",
"@angular/material": "^19.0.4", "@angular/material": "^19.0.4",
"@angular/platform-browser": "^19.0.5", "@angular/platform-browser": "^19.0.5",
"@angular/platform-browser-dynamic": "^19.0.5", "@angular/platform-browser-dynamic": "^19.0.5",
"@angular/platform-server": "^19.0.5", "@angular/platform-server": "^19.0.5",
"@angular/router": "^19.0.5", "@angular/router": "^19.0.5",
"@angular/ssr": "^19.0.6", "@angular/ssr": "^19.0.6",
"angular-oauth2-oidc": "^17.0.2", "angular-oauth2-oidc": "^17.0.2",
"express": "^4.18.2", "express": "^4.18.2",
"ngx-socket-io": "^4.7.0", "ngx-socket-io": "^4.7.0",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
"rxjs-websockets": "^9.0.0", "rxjs-websockets": "^9.0.0",
"tslib": "^2.3.0", "tslib": "^2.3.0",
"uuidv4": "^6.2.13", "uuidv4": "^6.2.13",
"zone.js": "~0.15.0" "zone.js": "~0.15.0"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "^19.0.6", "@angular-devkit/build-angular": "^19.0.6",
"@angular/cli": "^19.0.6", "@angular/cli": "^19.0.6",
"@angular/compiler-cli": "^19.0.5", "@angular/compiler-cli": "^19.0.5",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
"@types/jasmine": "~5.1.0", "@types/jasmine": "~5.1.0",
"@types/node": "^18.18.0", "@types/node": "^18.18.0",
"jasmine-core": "~5.1.0", "jasmine-core": "~5.1.0",
"karma": "~6.4.0", "karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0", "karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0", "karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0", "karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0", "karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.6.3" "typescript": "~5.6.3"
} }
} }

View File

@ -1,15 +1,16 @@
<mat-form-field> <mat-form-field>
<mat-label>Redeemable Action</mat-label> <mat-label>Redeemable Action</mat-label>
<input <input matInput
matInput type="text"
type="text" placeholder="Pick a Redeemable Action"
placeholder="Pick a Redeemable Action" aria-label="redeemable action"
aria-label="redeemable action" [formControl]="formControl"
[formControl]="formControl" [matAutocomplete]="auto"
[matAutocomplete]="auto" (blur)="blur()"
(blur)="blur()" (input)="input()">
(input)="input()"> <mat-autocomplete #auto="matAutocomplete"
<mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn" (optionSelected)="select($event.option.value)"> [displayWith]="displayFn"
(optionSelected)="select($event.option.value)">
@for (action of filteredActions; track action.name) { @for (action of filteredActions; track action.name) {
<mat-option [value]="action">{{action.name}}</mat-option> <mat-option [value]="action">{{action.name}}</mat-option>
} }

View File

@ -10,7 +10,7 @@ describe('ActionDropdownComponent', () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [ActionDropdownComponent] imports: [ActionDropdownComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(ActionDropdownComponent); fixture = TestBed.createComponent(ActionDropdownComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

View File

@ -8,11 +8,14 @@
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<form class="grid" [formGroup]="formGroup"> <form class="grid"
[formGroup]="formGroup">
<div class="item"> <div class="item">
<mat-form-field> <mat-form-field>
<mat-label>Redeemable Action Name</mat-label> <mat-label>Redeemable Action Name</mat-label>
<input matInput type="text" formControlName="name"> <input matInput
type="text"
formControlName="name">
@if (isNew && formGroup.get('name')?.invalid && (formGroup.get('name')?.dirty || @if (isNew && formGroup.get('name')?.invalid && (formGroup.get('name')?.dirty ||
formGroup.get('name')?.touched)) { formGroup.get('name')?.touched)) {
@if (formGroup.get('name')?.hasError('required')) { @if (formGroup.get('name')?.hasError('required')) {
@ -27,7 +30,9 @@
<div class="item"> <div class="item">
<mat-form-field> <mat-form-field>
<mat-label>Type</mat-label> <mat-label>Type</mat-label>
<mat-select matInput formControlName="type" (selectionChange)="action.type = $event.value"> <mat-select matInput
formControlName="type"
(selectionChange)="action.type = $event.value">
@for (type of actionTypes; track $index) { @for (type of actionTypes; track $index) {
<mat-option value="{{type}}">{{type}}</mat-option> <mat-option value="{{type}}">{{type}}</mat-option>
} }
@ -50,8 +55,11 @@
@if (field.type == 'text') { @if (field.type == 'text') {
<mat-form-field> <mat-form-field>
<mat-label>{{field.label}}</mat-label> <mat-label>{{field.label}}</mat-label>
<input matInput type="text" placeholder="{{field.placeholder}}" [formControl]="field.control" <input matInput
[(ngModel)]="action.data[field.key]"> type="text"
placeholder="{{field.placeholder}}"
[formControl]="field.control"
[(ngModel)]="action.data[field.key]">
@if (field.control.invalid && (field.control.dirty || field.control.touched)) { @if (field.control.invalid && (field.control.dirty || field.control.touched)) {
@if (field.control.hasError('required')) { @if (field.control.hasError('required')) {
<small class="error">This field is required.</small> <small class="error">This field is required.</small>
@ -65,7 +73,9 @@
@else if (field.type == 'number') { @else if (field.type == 'number') {
<mat-form-field> <mat-form-field>
<mat-label>{{field.label}}</mat-label> <mat-label>{{field.label}}</mat-label>
<input matInput type="number" [formControl]="field.control"> <input matInput
type="number"
[formControl]="field.control">
@if (field.control.invalid && (field.control.dirty || field.control.touched)) { @if (field.control.invalid && (field.control.dirty || field.control.touched)) {
@if (field.control.hasError('required')) { @if (field.control.hasError('required')) {
<small class="error">This field is required.</small> <small class="error">This field is required.</small>
@ -77,19 +87,19 @@
</mat-form-field> </mat-form-field>
} }
@else if (field.type == 'text-values') { @else if (field.type == 'text-values') {
<mat-form-field> <mat-form-field>
<mat-label>{{field.label}}</mat-label> <mat-label>{{field.label}}</mat-label>
<mat-select [formControl]="field.control"> <mat-select [formControl]="field.control">
@for (value of field.values; track $index) { @for (value of field.values; track $index) {
<mat-option [value]="value">{{value}}</mat-option> <mat-option [value]="value">{{value}}</mat-option>
}
</mat-select>
@if (field.control.invalid && (field.control.dirty || field.control.touched)) {
@if (field.control.hasError('required')) {
<small class="error">This field is required.</small>
} }
} </mat-select>
</mat-form-field> @if (field.control.invalid && (field.control.dirty || field.control.touched)) {
@if (field.control.hasError('required')) {
<small class="error">This field is required.</small>
}
}
</mat-form-field>
} }
</div> </div>
@ -98,17 +108,18 @@
} }
</mat-card-content> </mat-card-content>
<mat-card-actions class="actions" align="end"> <mat-card-actions class="actions"
align="end">
@if (!isNew) { @if (!isNew) {
<button mat-raised-button class="delete" (click)="deleteAction(action)">Delete</button> <button mat-raised-button
class="delete"
(click)="deleteAction(action)">Delete</button>
} }
<button <button mat-raised-button
mat-raised-button (click)="dialogRef.close()">Cancel</button>
(click)="dialogRef.close()">Cancel</button> <button mat-raised-button
<button disabled="{{!formsDirty || !formsValidity || waitForResponse}}"
mat-raised-button (click)="save()">Save</button>
disabled="{{!formsDirty || !formsValidity || waitForResponse}}"
(click)="save()">Save</button>
</mat-card-actions> </mat-card-actions>
</mat-card> </mat-card>
</body> </body>

View File

@ -25,6 +25,6 @@
color: #ba1a1a; color: #ba1a1a;
} }
.mdc-button ~ .mdc-button { .mdc-button~.mdc-button {
margin-left: 1em; margin-left: 1em;
} }

View File

@ -10,7 +10,7 @@ describe('ActionItemEditComponent', () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [ActionItemEditComponent] imports: [ActionItemEditComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(ActionItemEditComponent); fixture = TestBed.createComponent(ActionItemEditComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

View File

@ -1,11 +1,15 @@
<main> <main>
@for (action of actions; track $index) { @for (action of actions; track $index) {
<button type="button" class="container" (click)="modify(action)"> <button type="button"
class="container"
(click)="modify(action)">
<span class="title">{{action.name}}</span> <span class="title">{{action.name}}</span>
<span class="subtitle">{{action.type}}</span> <span class="subtitle">{{action.type}}</span>
</button> </button>
} }
<button type="button" class="container" (click)="create()"> <button type="button"
class="container"
(click)="create()">
<mat-icon>add</mat-icon> <mat-icon>add</mat-icon>
</button> </button>
</main> </main>

View File

@ -10,7 +10,7 @@ describe('ActionListComponent', () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [ActionListComponent] imports: [ActionListComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(ActionListComponent); fixture = TestBed.createComponent(ActionListComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

View File

@ -5,7 +5,8 @@
<article> <article>
<mat-form-field> <mat-form-field>
<mat-label>Filter by type</mat-label> <mat-label>Filter by type</mat-label>
<mat-select (selectionChange)="onFilterChange($event.value)" value="0"> <mat-select (selectionChange)="onFilterChange($event.value)"
value="0">
<mat-select-trigger> <mat-select-trigger>
<mat-icon matPrefix>filter_list</mat-icon>&nbsp;{{filter.name}} <mat-icon matPrefix>filter_list</mat-icon>&nbsp;{{filter.name}}
</mat-select-trigger> </mat-select-trigger>
@ -19,13 +20,15 @@
<mat-form-field> <mat-form-field>
<mat-label>Search</mat-label> <mat-label>Search</mat-label>
<input matInput <input matInput
type="text" type="text"
placeholder="Name of action" placeholder="Name of action"
[formControl]="searchControl" [formControl]="searchControl"
[(ngModel)]="search"> [(ngModel)]="search">
<mat-icon matPrefix>search</mat-icon> <mat-icon matPrefix>search</mat-icon>
</mat-form-field> </mat-form-field>
</article> </article>
</section> </section>
<action-list class="center" [actions]="actions" (actionsChange)="items.push($event)" /> <action-list class="center"
[actions]="actions"
(actionsChange)="items.push($event)" />
</body> </body>

View File

@ -1,4 +1,5 @@
body, h3 { body,
h3 {
padding: 0; padding: 0;
margin: 0; margin: 0;
} }
@ -21,14 +22,14 @@ section {
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
@media (max-width:1250px) { @media (max-width:1250px) {
display: block; display: block;
justify-content: center; justify-content: center;
} }
article { article {
display: flex; display: flex;
justify-content:space-around; justify-content: space-around;
} }
} }

View File

@ -10,7 +10,7 @@ describe('ActionsComponent', () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [ActionsComponent] imports: [ActionsComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(ActionsComponent); fixture = TestBed.createComponent(ActionsComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

View File

@ -1,4 +1,4 @@
<main class="main"> <main class="main">
<navigation class="navigation" /> <navigation class="navigation" />
<router-outlet class="content" /> <router-outlet class="content" />
</main> </main>

View File

@ -1,4 +1,4 @@
.main { .main {
display: grid; display: grid;
grid-template-columns: 20em 0px 1fr; grid-template-columns: 20em 0px 1fr;
} }

View File

@ -12,9 +12,9 @@ export const appConfig: ApplicationConfig = {
provideZoneChangeDetection({ eventCoalescing: true }), provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes), provideRouter(routes),
provideHttpClient( provideHttpClient(
withInterceptors([(req: HttpRequest<unknown>, next: HttpHandlerFn) => { withInterceptors([(req: HttpRequest<unknown>, next: HttpHandlerFn) => {
return next(req); return next(req);
}]) }])
), ),
provideOAuthClient(), provideOAuthClient(),
provideClientHydration(), provideAnimationsAsync() provideClientHydration(), provideAnimationsAsync()

View File

@ -2,7 +2,8 @@
<main> <main>
<mat-form-field> <mat-form-field>
<mat-label>User to impersonate</mat-label> <mat-label>User to impersonate</mat-label>
<mat-select (selectionChange)="onChange($event)" [(value)]="impersonated"> <mat-select (selectionChange)="onChange($event)"
[(value)]="impersonated">
<mat-option>{{getUsername()}}</mat-option> <mat-option>{{getUsername()}}</mat-option>
@for (user of users; track user.id) { @for (user of users; track user.id) {
<mat-option [value]="user.id">{{ user.name }}</mat-option> <mat-option [value]="user.id">{{ user.name }}</mat-option>

View File

@ -10,7 +10,7 @@ describe('ImpersonationComponent', () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [ImpersonationComponent] imports: [ImpersonationComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(ImpersonationComponent); fixture = TestBed.createComponent(ImpersonationComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

View File

@ -1,5 +1,6 @@
<div class="login"> <div class="login">
<mat-card class="outer" appearance="outlined"> <mat-card class="outer"
appearance="outlined">
<mat-card-header> <mat-card-header>
<h1 class="title">Login</h1> <h1 class="title">Login</h1>
</mat-card-header> </mat-card-header>
@ -8,7 +9,9 @@
<p>Log in with your favorite livestream service</p> <p>Log in with your favorite livestream service</p>
<a> <a>
<mat-card appearance="outlined" class="twitch" (click)="login()"> <mat-card appearance="outlined"
class="twitch"
(click)="login()">
<mat-card-content> <mat-card-content>
Twitch Twitch
</mat-card-content> </mat-card-content>

View File

@ -10,7 +10,7 @@ describe('LoginComponent', () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [LoginComponent] imports: [LoginComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(LoginComponent); fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

View File

@ -5,29 +5,29 @@ import { Subscription } from 'rxjs';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
@Component({ @Component({
selector: 'login', selector: 'login',
standalone: true, standalone: true,
imports: [MatCardModule, RouterModule], imports: [MatCardModule, RouterModule],
templateUrl: './login.component.html', templateUrl: './login.component.html',
styleUrl: './login.component.scss' styleUrl: './login.component.scss'
}) })
export class LoginComponent implements OnInit, OnDestroy { export class LoginComponent implements OnInit, OnDestroy {
subscription: Subscription | null; subscription: Subscription | null;
constructor(private router: Router) { constructor(private router: Router) {
this.subscription = null; this.subscription = null;
} }
ngOnInit(): void { ngOnInit(): void {
} }
ngOnDestroy(): void { ngOnDestroy(): void {
if (this.subscription) if (this.subscription)
this.subscription.unsubscribe() this.subscription.unsubscribe()
} }
login() { login() {
document.location.replace(environment.API_HOST + '/auth'); document.location.replace(environment.API_HOST + '/auth');
} }
} }

View File

@ -17,7 +17,8 @@
</mat-form-field> </mat-form-field>
</mat-card-content> </mat-card-content>
<mat-card-actions align="end"> <mat-card-actions align="end">
<button mat-raised-button (click)="login()">Log In</button> <button mat-raised-button
(click)="login()">Log In</button>
</mat-card-actions> </mat-card-actions>
</mat-card> </mat-card>
</main> </main>

View File

@ -10,7 +10,7 @@ describe('TtsLoginComponent', () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [TtsLoginComponent] imports: [TtsLoginComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(TtsLoginComponent); fixture = TestBed.createComponent(TtsLoginComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

View File

@ -1,6 +1,7 @@
@if (auth.isAuthenticated()) { @if (auth.isAuthenticated()) {
<main> <main>
<mat-card appearance="outlined" class="card"> <mat-card appearance="outlined"
class="card">
<mat-card-header> <mat-card-header>
<mat-card-title>{{username}}</mat-card-title> <mat-card-title>{{username}}</mat-card-title>
</mat-card-header> </mat-card-header>
@ -10,9 +11,11 @@
<mat-card-actions class="actions"> <mat-card-actions class="actions">
<div> <div>
@if (isTTSLoggedIn) { @if (isTTSLoggedIn) {
<button mat-raised-button (click)="client.disconnect()"><span class="disconnect">Disconnect</span></button> <button mat-raised-button
(click)="client.disconnect()"><span class="disconnect">Disconnect</span></button>
} }
<button mat-raised-button (click)="auth.logout()"><span class="logoff">Log Off</span></button> <button mat-raised-button
(click)="auth.logout()"><span class="logoff">Log Off</span></button>
</div> </div>
</mat-card-actions> </mat-card-actions>
</mat-card> </mat-card>

View File

@ -14,10 +14,11 @@ main {
justify-content: center; justify-content: center;
} }
.disconnect, .logoff { .disconnect,
.logoff {
color: red; color: red;
} }
.mdc-button ~ .mdc-button { .mdc-button~.mdc-button {
margin-left: 1em; margin-left: 1em;
} }

View File

@ -10,7 +10,7 @@ describe('UserCardComponent', () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [UserCardComponent] imports: [UserCardComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(UserCardComponent); fixture = TestBed.createComponent(UserCardComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

View File

@ -1,17 +1,18 @@
<mat-form-field> <mat-form-field>
<mat-label>Group</mat-label> <mat-label>Group</mat-label>
<input <input matInput
matInput type="text"
type="text" placeholder="Pick a group"
placeholder="Pick a group" aria-label="group"
aria-label="group" [formControl]="formControl"
[formControl]="formControl" [matAutocomplete]="auto"
[matAutocomplete]="auto" [disabled]="!!groupDisabled"
[disabled]="!!groupDisabled" [readonly]="!!groupDisabled"
[readonly]="!!groupDisabled" (blur)="blur()"
(blur)="blur()" (input)="input()">
(input)="input()"> <mat-autocomplete #auto="matAutocomplete"
<mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn" (optionSelected)="select($event.option.value)"> [displayWith]="displayFn"
(optionSelected)="select($event.option.value)">
@for (group of filteredGroups; track group.id) { @for (group of filteredGroups; track group.id) {
<mat-option [value]="group">{{group.name}}</mat-option> <mat-option [value]="group">{{group.name}}</mat-option>
} }

View File

@ -10,7 +10,7 @@ describe('GroupDropdownComponent', () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [GroupDropdownComponent] imports: [GroupDropdownComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(GroupDropdownComponent); fixture = TestBed.createComponent(GroupDropdownComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

View File

@ -89,7 +89,7 @@ export class GroupDropdownComponent implements OnInit {
//this.groupChange.emit(newValue.name); //this.groupChange.emit(newValue.name);
} else if (!newValue) } else if (!newValue)
this.formControl.setValue(undefined); this.formControl.setValue(undefined);
//this.groupChange.emit(undefined); //this.groupChange.emit(undefined);
} }
} }

View File

@ -7,7 +7,10 @@
<mat-card-content> <mat-card-content>
<mat-form-field> <mat-form-field>
<mat-label>Group Name</mat-label> <mat-label>Group Name</mat-label>
<input matInput type="text" [formControl]="nameForm" [disabled]="isSpecial" /> <input matInput
type="text"
[formControl]="nameForm"
[disabled]="isSpecial" />
@if (nameForm.invalid && (nameForm.dirty || nameForm.touched)) { @if (nameForm.invalid && (nameForm.dirty || nameForm.touched)) {
@if (nameForm.hasError('required')) { @if (nameForm.hasError('required')) {
<small class="error">This field is required.</small> <small class="error">This field is required.</small>
@ -16,7 +19,9 @@
</mat-form-field> </mat-form-field>
<mat-form-field> <mat-form-field>
<mat-label>TTS Priority</mat-label> <mat-label>TTS Priority</mat-label>
<input matInput type="number" [formControl]="priorityForm" /> <input matInput
type="number"
[formControl]="priorityForm" />
@if (priorityForm.invalid && (priorityForm.dirty || priorityForm.touched)) { @if (priorityForm.invalid && (priorityForm.dirty || priorityForm.touched)) {
@if (priorityForm.hasError('required')) { @if (priorityForm.hasError('required')) {
<small class="error">This field is required.</small> <small class="error">This field is required.</small>
@ -34,16 +39,14 @@
</mat-form-field> </mat-form-field>
</mat-card-content> </mat-card-content>
<mat-card-actions> <mat-card-actions>
<button <button mat-button
mat-button [disabled]="waitForResponse || formGroup.invalid"
[disabled]="waitForResponse || formGroup.invalid" (click)="add()">
(click)="add()">
<mat-icon>add</mat-icon>Add <mat-icon>add</mat-icon>Add
</button> </button>
<button <button mat-button
mat-button [disabled]="waitForResponse"
[disabled]="waitForResponse" (click)="cancel()">
(click)="cancel()">
<mat-icon>cancel</mat-icon>Cancel <mat-icon>cancel</mat-icon>Cancel
</button> </button>
</mat-card-actions> </mat-card-actions>

View File

@ -10,7 +10,7 @@ describe('GroupItemEditComponent', () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [GroupItemEditComponent] imports: [GroupItemEditComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(GroupItemEditComponent); fixture = TestBed.createComponent(GroupItemEditComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

View File

@ -14,7 +14,8 @@
<small class="muted block">polic{{item().chatters.length == 1 ? 'y' : 'ies'}}</small> <small class="muted block">polic{{item().chatters.length == 1 ? 'y' : 'ies'}}</small>
</section> </section>
<section> <section>
<button mat-button (click)="router.navigate([link])"> <button mat-button
(click)="router.navigate([link])">
<mat-icon>pageview</mat-icon>View <mat-icon>pageview</mat-icon>View
</button> </button>
</section> </section>

View File

@ -10,7 +10,7 @@ describe('GroupItemComponent', () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [GroupItemComponent] imports: [GroupItemComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(GroupItemComponent); fixture = TestBed.createComponent(GroupItemComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

View File

@ -1,7 +1,7 @@
<ul> <ul>
@for (group of groups; track $index) { @for (group of groups; track $index) {
<li> <li>
<group-item [item]="group" /> <group-item [item]="group" />
</li> </li>
} }
</ul> </ul>

View File

@ -10,7 +10,7 @@ describe('GroupListComponent', () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [GroupListComponent] imports: [GroupListComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(GroupListComponent); fixture = TestBed.createComponent(GroupListComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

View File

@ -8,7 +8,10 @@
{{policies.length}} polic{{policies.length == 1 ? 'y' : 'ies'}} {{policies.length}} polic{{policies.length == 1 ? 'y' : 'ies'}}
</mat-panel-description> </mat-panel-description>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<policy-add-button class="add" [groups]="groups" [policies]="policies" [group]="group?.id" /> <policy-add-button class="add"
[groups]="groups"
[policies]="policies"
[group]="group?.id" />
@if (policies.length > 0) { @if (policies.length > 0) {
<policy-table [policies]="policies" /> <policy-table [policies]="policies" />
} }
@ -28,7 +31,9 @@
<p>Deleting this group will delete everything that is part of it, including policies and permissions.</p> <p>Deleting this group will delete everything that is part of it, including policies and permissions.</p>
</article> </article>
<article class="right"> <article class="right">
<button mat-raised-button class="delete" (click)="delete()"> <button mat-raised-button
class="delete"
(click)="delete()">
<mat-icon>delete</mat-icon>Delete this group. <mat-icon>delete</mat-icon>Delete this group.
</button> </button>
</article> </article>

View File

@ -1,4 +1,4 @@
.mat-expansion-panel ~ .mat-expansion-panel { .mat-expansion-panel~.mat-expansion-panel {
margin-top: 4em; margin-top: 4em;
} }

View File

@ -10,7 +10,7 @@ describe('GroupPageComponent', () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [GroupPageComponent] imports: [GroupPageComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(GroupPageComponent); fixture = TestBed.createComponent(GroupPageComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

View File

@ -1,15 +1,21 @@
<button mat-button [mat-menu-trigger-for]="menu"> <button mat-button
[mat-menu-trigger-for]="menu">
<mat-icon>add</mat-icon> <mat-icon>add</mat-icon>
Add a group Add a group
</button> </button>
<mat-menu #menu="matMenu"> <mat-menu #menu="matMenu">
<button mat-menu-item (click)="openDialog('')">Custom Group</button> <button mat-menu-item
<button mat-menu-item (click)="openDialog('everyone')">Everyone Group</button> (click)="openDialog('')">Custom Group</button>
<button mat-menu-item (click)="openDialog('subscribers')">Subscriber Group</button> <button mat-menu-item
<button mat-menu-item (click)="openDialog('moderators')">Moderator Group</button> (click)="openDialog('everyone')">Everyone Group</button>
<button mat-menu-item (click)="openDialog('vip')">VIP Group</button> <button mat-menu-item
<button mat-menu-item (click)="openDialog('broadcaster')">Broadcaster Group</button> (click)="openDialog('subscribers')">Subscriber Group</button>
<button mat-menu-item
(click)="openDialog('moderators')">Moderator Group</button>
<button mat-menu-item
(click)="openDialog('vip')">VIP Group</button>
<button mat-menu-item
(click)="openDialog('broadcaster')">Broadcaster Group</button>
</mat-menu> </mat-menu>
<group-list <group-list class="groups"
class="groups" [groups]="items" />
[groups]="items" />

View File

@ -10,7 +10,7 @@ describe('GroupsComponent', () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [GroupsComponent] imports: [GroupsComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(GroupsComponent); fixture = TestBed.createComponent(GroupsComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

View File

@ -44,14 +44,14 @@ export class HermesClientService {
this.events.emit('tts_logoff', null); this.events.emit('tts_logoff', null);
} }
public filter(predicate: (data: any) => boolean): Observable<any>|undefined { public filter(predicate: (data: any) => boolean): Observable<any> | undefined {
return this.socket.get$()?.pipe( return this.socket.get$()?.pipe(
filter(predicate), filter(predicate),
map(d => d.d) map(d => d.d)
); );
} }
public filterByRequestType(requestName: string): Observable<any>|undefined { public filterByRequestType(requestName: string): Observable<any> | undefined {
return this.socket.get$()?.pipe( return this.socket.get$()?.pipe(
filter(d => d.op == 4 && d.d.request.type === requestName), filter(d => d.op == 4 && d.d.request.type === requestName),
map(d => d.d) map(d => d.d)

View File

@ -3,45 +3,52 @@
<ul> <ul>
@if (!isLoggedIn()) { @if (!isLoggedIn()) {
<li> <li>
<a routerLink="/login" routerLinkActive="active"> <a routerLink="/login"
routerLinkActive="active">
Login Login
</a> </a>
</li> </li>
} }
@if (isLoggedIn() && !isTTSLoggedIn()) { @if (isLoggedIn() && !isTTSLoggedIn()) {
<li> <li>
<a routerLink="/tts-login" routerLinkActive="active"> <a routerLink="/tts-login"
routerLinkActive="active">
TTS Login TTS Login
</a> </a>
</li> </li>
} }
@if (isLoggedIn() && isTTSLoggedIn()) { @if (isLoggedIn() && isTTSLoggedIn()) {
<li> <li>
<a routerLink="/policies" routerLinkActive="active"> <a routerLink="/policies"
routerLinkActive="active">
Policies Policies
</a> </a>
</li> </li>
<li> <li>
<a routerLink="/filters" routerLinkActive="active"> <a routerLink="/filters"
routerLinkActive="active">
Filters Filters
</a> </a>
</li> </li>
<li> <li>
<a routerLink="/actions" routerLinkActive="active"> <a routerLink="/actions"
routerLinkActive="active">
Actions Actions
</a> </a>
</li> </li>
<li> <li>
<a routerLink="/redemptions" routerLinkActive="active"> <a routerLink="/redemptions"
routerLinkActive="active">
Redemptions Redemptions
</a> </a>
</li> </li>
@if (isAdmin()) { @if (isAdmin()) {
<li> <li>
<a routerLink="/groups" routerLinkActive="active"> <a routerLink="/groups"
Groups routerLinkActive="active">
</a> Groups
</li> </a>
</li>
} }
} }
</ul> </ul>

View File

@ -5,33 +5,33 @@ $secondary_background_color: #DDDDDD;
$secondary_font_color: #333333; $secondary_font_color: #333333;
ul { ul {
padding: 0; padding: 0;
} }
li { li {
list-style: none; list-style: none;
width: 100%; width: 100%;
display: flex; display: flex;
flex-grow: 1; flex-grow: 1;
justify-content: center; justify-content: center;
flex-direction: column; flex-direction: column;
} }
a { a {
background-color: transparent; background-color: transparent;
padding: 1em; padding: 1em;
border: 0; border: 0;
margin: 0; margin: 0;
font-size: large; font-size: large;
text-decoration: none; text-decoration: none;
color: $primary_font_color; color: $primary_font_color;
border-radius: 10px; border-radius: 10px;
} }
a:hover { a:hover {
background-color: #FAFAFA; background-color: #FAFAFA;
} }
a.active { a.active {
background-color: #F5F5F5; background-color: #F5F5F5;
} }

View File

@ -10,7 +10,7 @@ describe('NavigationComponent', () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [NavigationComponent] imports: [NavigationComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(NavigationComponent); fixture = TestBed.createComponent(NavigationComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

View File

@ -3,6 +3,7 @@ import { PolicyComponent } from './policy/policy.component';
import { PolicyTableComponent } from './policy-table/policy-table.component'; import { PolicyTableComponent } from './policy-table/policy-table.component';
import { PolicyItemEditComponent } from './policy-item-edit/policy-item-edit.component'; import { PolicyItemEditComponent } from './policy-item-edit/policy-item-edit.component';
import { PolicyAddButtonComponent } from './policy-add-button/policy-add-button.component'; import { PolicyAddButtonComponent } from './policy-add-button/policy-add-button.component';
import { PolicyAddFormComponent } from './policy-add-form/policy-add-form.component';
@NgModule({ @NgModule({
@ -12,6 +13,7 @@ import { PolicyAddButtonComponent } from './policy-add-button/policy-add-button.
PolicyTableComponent, PolicyTableComponent,
PolicyAddButtonComponent, PolicyAddButtonComponent,
PolicyItemEditComponent, PolicyItemEditComponent,
PolicyAddFormComponent,
] ]
}) })
export class PoliciesModule { } export class PoliciesModule { }

View File

@ -1,4 +1,5 @@
<button mat-button (click)="openDialog()"> <button mat-button
(click)="openDialog()">
<mat-icon>add</mat-icon> <mat-icon>add</mat-icon>
Add a policy Add a policy
</button> </button>

View File

@ -10,7 +10,7 @@ describe('PolicyAddButtonComponent', () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [PolicyAddButtonComponent] imports: [PolicyAddButtonComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(PolicyAddButtonComponent); fixture = TestBed.createComponent(PolicyAddButtonComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

View File

@ -19,7 +19,7 @@ export class PolicyAddButtonComponent {
private readonly dialog = inject(MatDialog); private readonly dialog = inject(MatDialog);
@Input({ required: true }) policies: Policy[] = []; @Input({ required: true }) policies: Policy[] = [];
@Input({ required: true }) groups: Group[] = []; @Input({ required: true }) groups: Group[] = [];
@Input() group: string|undefined = undefined; @Input() group: string | undefined = undefined;
@Output() policy = new EventEmitter<Policy>(); @Output() policy = new EventEmitter<Policy>();

View File

@ -1,25 +1,17 @@
<div> <form standalone>
<form <mat-form-field>
standalone> <mat-label>Path</mat-label>
<mat-form-field> <input name="path"
<input type="text"
name="path" placeholder="Pick one"
type="text" [(ngModel)]="policy"
placeholder="Pick one" matInput
[(ngModel)]="newPolicyName" [formControl]="policyControl"
matInput [matAutocomplete]="auto" />
[formControl]="myControl" <mat-autocomplete #auto="matAutocomplete">
[matAutocomplete]="auto" /> @for (option of filteredPolicies | async; track option) {
<mat-autocomplete #auto="matAutocomplete"> <mat-option [value]="option">{{option}}</mat-option>
@for (option of filteredPolicies | async; track option) { }
<mat-option [value]="option">{{option}}</mat-option> </mat-autocomplete>
} </mat-form-field>
</mat-autocomplete> </form>
</mat-form-field>
<button
mat-flat-button
(click)="addNewPolicy()">
Add
</button>
</form>
</div>

View File

@ -10,7 +10,7 @@ describe('PolicyAddFormComponent', () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [PolicyAddFormComponent] imports: [PolicyAddFormComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(PolicyAddFormComponent); fixture = TestBed.createComponent(PolicyAddFormComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

View File

@ -4,71 +4,68 @@ import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'
import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import EventService from '../../shared/services/EventService';
import { map, Observable, startWith } from 'rxjs'; import { map, Observable, startWith } from 'rxjs';
import { HermesClientService } from '../../hermes-client.service';
const Policies = [ const Policies = [
{ path: "tts", description: "Anything to do with TTS" }, { path: "tts", description: "Anything to do with TTS" },
{ path: "tts.chat", description: "Anything to do with chat" }, { path: "tts.chat", description: "Anything to do with chat" },
{ path: "tts.chat.bits.read", description: "To read chat messages with bits via TTS" }, { path: "tts.chat.bits.read", description: "To read chat messages with bits via TTS" },
{ path: "tts.chat.messages.read", description: "To read chat messages via TTS" }, { path: "tts.chat.messages.read", description: "To read chat messages via TTS" },
{ path: "tts.chat.redemptions.read", description: "To read channel point redemption messages via TTS" }, { path: "tts.chat.redemptions.read", description: "To read channel point redemption messages via TTS" },
{ path: "tts.chat.subscriptions.read", description: "To read chat messages from subscriptions via TTS" }, { path: "tts.chat.subscriptions.read", description: "To read chat messages from subscriptions via TTS" },
{ path: "tts.commands", description: "To execute commands for TTS" }, { path: "tts.commands", description: "To execute commands for TTS" },
{ path: "tts.commands.nightbot", description: "To use !nightbot command" }, { path: "tts.commands.nightbot", description: "To use !nightbot command" },
{ path: "tts.commands.obs", description: "To use !obs command" }, { path: "tts.commands.obs", description: "To use !obs command" },
{ path: "tts.commands.refresh", description: "To use !refresh command" }, { path: "tts.commands.refresh", description: "To use !refresh command" },
{ path: "tts.commands.skip", description: "To use !skip command" }, { path: "tts.commands.skip", description: "To use !skip command" },
{ path: "tts.commands.skipall", description: "To use !skipall command" }, { path: "tts.commands.skipall", description: "To use !skipall command" },
{ path: "tts.commands.tts", description: "To use !tts command" }, { path: "tts.commands.tts", description: "To use !tts command" },
{ path: "tts.commands.tts.join", description: "To use !tts join command" }, { path: "tts.commands.tts.join", description: "To use !tts join command" },
{ path: "tts.commands.tts.leave", description: "To use !tts leave command" }, { path: "tts.commands.tts.leave", description: "To use !tts leave command" },
{ path: "tts.commands.version", description: "To use !version command" }, { path: "tts.commands.version", description: "To use !version command" },
{ path: "tts.commands.voice", description: "To use !voice command" }, { path: "tts.commands.voice", description: "To use !voice command" },
{ path: "tts.commands.voice.admin", description: "To use !voice command on others" }, { path: "tts.commands.voice.admin", description: "To use !voice command on others" },
] ]
@Component({ @Component({
selector: 'policy-add-form', selector: 'policy-add-form',
imports: [ imports: [
AsyncPipe, AsyncPipe,
FormsModule, FormsModule,
MatAutocompleteModule, MatAutocompleteModule,
MatButtonModule, MatButtonModule,
MatInputModule, MatInputModule,
ReactiveFormsModule, ReactiveFormsModule,
], ],
templateUrl: './policy-add-form.component.html', templateUrl: './policy-add-form.component.html',
styleUrl: './policy-add-form.component.scss' styleUrl: './policy-add-form.component.scss'
}) })
export class PolicyAddFormComponent { export class PolicyAddFormComponent {
myControl = new FormControl(''); policyControl = new FormControl('');
newPolicyName: string = ''; policy: string = '';
filteredPolicies: Observable<string[]>; filteredPolicies: Observable<string[]>;
constructor(private events: EventService, private hermes: HermesClientService) { constructor() {
this.filteredPolicies = this.myControl.valueChanges.pipe( this.filteredPolicies = this.policyControl.valueChanges.pipe(
startWith(''), startWith(''),
map(value => this._filter(value || '')), map(value => this._filter(value || '')),
); );
}
ngOnInit() {
this.filteredPolicies = this.policyControl.valueChanges.pipe(
startWith(''),
map(value => this._filter(value || '')),
);
}
private _filter(value: string): string[] {
const filterValue = value.toLowerCase();
const names = Policies.map(p => p.path);
if (names.includes(filterValue)) {
return names;
} }
ngOnInit() { return names.filter(option => option.toLowerCase().includes(filterValue));
this.filteredPolicies = this.myControl.valueChanges.pipe( }
startWith(''),
map(value => this._filter(value || '')),
);
}
private _filter(value: string): string[] {
const filterValue = value.toLowerCase();
return Policies.map(p => p.path).filter(option => option.toLowerCase().includes(filterValue));
}
addNewPolicy() {
this.events.emit('addPolicy', this.newPolicyName);
this.newPolicyName = "";
}
} }

View File

@ -3,16 +3,17 @@
<mat-card-title>{{isNew ? 'Add' : 'Edit'}} Policy</mat-card-title> <mat-card-title>{{isNew ? 'Add' : 'Edit'}} Policy</mat-card-title>
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<group-dropdown <group-dropdown ngDefaultControl
ngDefaultControl [formControl]="groupControl"
[formControl]="groupControl" [groups]="data.groups"
[groups]="data.groups" [group]="data.group_id"
[group]="data.group_id" [groupDisabled]="data.groupDisabled"
[groupDisabled]="data.groupDisabled" [errorMessages]="groupErrorMessages" />
[errorMessages]="groupErrorMessages" />
<mat-form-field> <mat-form-field>
<mat-label>Path</mat-label> <mat-label>Path</mat-label>
<input matInput placeholder="Path" [formControl]="pathControl" /> <input matInput
placeholder="Path"
[formControl]="pathControl" />
@if (pathControl.invalid && (pathControl.dirty || pathControl.touched)) { @if (pathControl.invalid && (pathControl.dirty || pathControl.touched)) {
@if (pathControl.hasError('required')) { @if (pathControl.hasError('required')) {
<small class="error">This field is required.</small> <small class="error">This field is required.</small>
@ -21,7 +22,9 @@
</mat-form-field> </mat-form-field>
<mat-form-field> <mat-form-field>
<mat-label>Usage</mat-label> <mat-label>Usage</mat-label>
<input matInput type="number" [formControl]="usageControl" /> <input matInput
type="number"
[formControl]="usageControl" />
@if (usageControl.invalid && (usageControl.dirty || usageControl.touched)) { @if (usageControl.invalid && (usageControl.dirty || usageControl.touched)) {
@if (usageControl.hasError('required')) { @if (usageControl.hasError('required')) {
<small class="error">This field is required.</small> <small class="error">This field is required.</small>
@ -39,7 +42,9 @@
</mat-form-field> </mat-form-field>
<mat-form-field> <mat-form-field>
<mat-label>Span</mat-label> <mat-label>Span</mat-label>
<input matInput type="number" [formControl]="spanControl" /> <input matInput
type="number"
[formControl]="spanControl" />
@if (spanControl.invalid && (spanControl.dirty || spanControl.touched)) { @if (spanControl.invalid && (spanControl.dirty || spanControl.touched)) {
@if (spanControl.hasError('required')) { @if (spanControl.hasError('required')) {
<small class="error">This field is required.</small> <small class="error">This field is required.</small>
@ -58,15 +63,18 @@
</mat-card-content> </mat-card-content>
<mat-card-actions> <mat-card-actions>
@if (isNew) { @if (isNew) {
<button mat-button (click)="save()"> <button mat-button
(click)="save()">
<mat-icon>add</mat-icon>Add <mat-icon>add</mat-icon>Add
</button> </button>
} @else { } @else {
<button mat-button (click)="save()"> <button mat-button
(click)="save()">
<mat-icon>save</mat-icon>Save <mat-icon>save</mat-icon>Save
</button> </button>
} }
<button mat-button (click)="dialogRef.close()"> <button mat-button
(click)="dialogRef.close()">
<mat-icon>cancel</mat-icon>Cancel <mat-icon>cancel</mat-icon>Cancel
</button> </button>
</mat-card-actions> </mat-card-actions>

View File

@ -10,7 +10,7 @@ describe('PolicyItemEditComponent', () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [PolicyItemEditComponent] imports: [PolicyItemEditComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(PolicyItemEditComponent); fixture = TestBed.createComponent(PolicyItemEditComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

View File

@ -10,6 +10,7 @@ import { HermesClientService } from '../../hermes-client.service';
import { GroupDropdownComponent } from '../../groups/group-dropdown/group-dropdown.component'; import { GroupDropdownComponent } from '../../groups/group-dropdown/group-dropdown.component';
import { Group } from '../../shared/models/group'; import { Group } from '../../shared/models/group';
import { Policy } from '../../shared/models/policy'; import { Policy } from '../../shared/models/policy';
import { PolicyAddFormComponent } from '../policy-add-form/policy-add-form.component';
@Component({ @Component({
selector: 'policy-item-edit', selector: 'policy-item-edit',
@ -21,6 +22,7 @@ import { Policy } from '../../shared/models/policy';
MatIconModule, MatIconModule,
MatInputModule, MatInputModule,
ReactiveFormsModule, ReactiveFormsModule,
PolicyAddFormComponent
], ],
templateUrl: './policy-item-edit.component.html', templateUrl: './policy-item-edit.component.html',
styleUrl: './policy-item-edit.component.scss' styleUrl: './policy-item-edit.component.scss'

View File

@ -1,40 +1,59 @@
<table mat-table [dataSource]="policies" class="mat-elevation-z8"> <table mat-table
[dataSource]="policies"
class="mat-elevation-z8">
<ng-container matColumnDef="path"> <ng-container matColumnDef="path">
<th mat-header-cell *matHeaderCellDef>Path</th> <th mat-header-cell
<td mat-cell *matCellDef="let policy"> *matHeaderCellDef>Path</th>
<td mat-cell
*matCellDef="let policy">
{{policy.path}} {{policy.path}}
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="group"> <ng-container matColumnDef="group">
<th mat-header-cell *matHeaderCellDef>Group</th> <th mat-header-cell
<td mat-cell *matCellDef="let policy"> *matHeaderCellDef>Group</th>
<td mat-cell
*matCellDef="let policy">
{{getGroupById(policy.group_id)?.name || '\<unknown group\>'}} {{getGroupById(policy.group_id)?.name || '\<unknown group\>'}}
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="usage"> <ng-container matColumnDef="usage">
<th mat-header-cell *matHeaderCellDef>Usage Rate</th> <th mat-header-cell
<td mat-cell class="center" *matCellDef="let policy"> *matHeaderCellDef>Usage Rate</th>
<td mat-cell
class="center"
*matCellDef="let policy">
{{policy.usage}} {{policy.usage}}
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="span"> <ng-container matColumnDef="span">
<th mat-header-cell *matHeaderCellDef>Span (ms)</th> <th mat-header-cell
<td mat-cell class="center" *matCellDef="let policy"> *matHeaderCellDef>Span (ms)</th>
<td mat-cell
class="center"
*matCellDef="let policy">
{{policy.span}} {{policy.span}}
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> Actions </th> <th mat-header-cell
<td mat-cell *matCellDef="let policy"> *matHeaderCellDef> Actions </th>
<button mat-button (click)="edit(policy)"><mat-icon>edit</mat-icon>Edit</button> <td mat-cell
<button mat-button class="delete" (click)="delete(policy)"><mat-icon>delete</mat-icon>Delete</button> *matCellDef="let policy">
<button mat-button
(click)="edit(policy)"><mat-icon>edit</mat-icon>Edit</button>
<button mat-button
class="delete"
(click)="delete(policy)"><mat-icon>delete</mat-icon>Delete</button>
</td> </td>
</ng-container> </ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> <tr mat-header-row
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row
*matRowDef="let row; columns: displayedColumns;"></tr>
</table> </table>

View File

@ -7,6 +7,6 @@ table {
color: red; color: red;
} }
button ~ button { button~button {
margin-left: 1em; margin-left: 1em;
} }

View File

@ -10,7 +10,7 @@ describe('PolicyTableComponent', () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [PolicyTableComponent] imports: [PolicyTableComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(PolicyTableComponent); fixture = TestBed.createComponent(PolicyTableComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

View File

@ -1,6 +1,8 @@
<h4>Policies</h4> <h4>Policies</h4>
<div class="add"> <div class="add">
<policy-add-button [policies]="policies" [groups]="groups" (policy)="addPolicy($event)" /> <policy-add-button [policies]="policies"
[groups]="groups"
(policy)="addPolicy($event)" />
</div> </div>
<div> <div>

View File

@ -1,5 +1,5 @@
h4 { h4 {
text-align: center; text-align: center;
} }
.add { .add {

View File

@ -10,7 +10,7 @@ describe('PolicyComponent', () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [PolicyComponent] imports: [PolicyComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(PolicyComponent); fixture = TestBed.createComponent(PolicyComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

View File

@ -5,23 +5,24 @@
</mat-card-title> </mat-card-title>
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<twitch-redemption-dropdown <twitch-redemption-dropdown ngDefaultControl
ngDefaultControl [formControl]="redemptionFormControl"
[formControl]="redemptionFormControl" [errorMessages]="redemptionErrorMessages"
[errorMessages]="redemptionErrorMessages" [twitchRedemptions]="twitchRedemptions"
[twitchRedemptions]="twitchRedemptions" [(twitchRedemptionId)]="redemption.redemption_id" />
[(twitchRedemptionId)]="redemption.redemption_id" />
<action-dropdown <action-dropdown ngDefaultControl
ngDefaultControl [formControl]="actionFormControl"
[formControl]="actionFormControl" [errorMessages]="actionErrorMessages"
[errorMessages]="actionErrorMessages" [actions]="redeemableActions"
[actions]="redeemableActions" [(action)]="redemption.action_name" />
[(action)]="redemption.action_name" />
<mat-form-field> <mat-form-field>
<mat-label>Order</mat-label> <mat-label>Order</mat-label>
<input matInput type="number" [formControl]="orderFormControl" [value]="redemption.order" /> <input matInput
type="number"
[formControl]="orderFormControl"
[value]="redemption.order" />
@if (orderFormControl.invalid && (orderFormControl.dirty || orderFormControl.touched)) { @if (orderFormControl.invalid && (orderFormControl.dirty || orderFormControl.touched)) {
@for (error of orderErrorMessageKeys; track $index) { @for (error of orderErrorMessageKeys; track $index) {
@if (orderFormControl.hasError(error)) { @if (orderFormControl.hasError(error)) {
@ -31,19 +32,17 @@
} }
</mat-form-field> </mat-form-field>
<div class="buttons"> <div class="buttons">
<button <button mat-icon-button
mat-icon-button class="save"
class="save" [disabled]="waitForResponse || formGroups.invalid"
[disabled]="waitForResponse || formGroups.invalid" (click)="save()">
(click)="save()">
<mat-icon>save</mat-icon> <mat-icon>save</mat-icon>
</button> </button>
@if (redemption.id) { @if (redemption.id) {
<button <button mat-icon-button
mat-icon-button class="delete"
class="delete" [disabled]="waitForResponse"
[disabled]="waitForResponse" (click)="delete()">
(click)="delete()">
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
</button> </button>
} }

View File

@ -42,7 +42,7 @@ button,
margin: 0; margin: 0;
} }
button ~ button { button~button {
margin-left: 2em; margin-left: 2em;
} }

View File

@ -10,7 +10,7 @@ describe('RedemptionItemEditComponent', () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [RedemptionItemEditComponent] imports: [RedemptionItemEditComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(RedemptionItemEditComponent); fixture = TestBed.createComponent(RedemptionItemEditComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

View File

@ -1,8 +1,11 @@
<div class="content"> <div class="content">
<button mat-button class="add" (click)="add()"><mat-icon>add</mat-icon> Add Redemption</button> <button mat-button
class="add"
(click)="add()"><mat-icon>add</mat-icon> Add Redemption</button>
<mat-expansion-panel class="filters-expander" (opened)="panelOpenState.set(true)" <mat-expansion-panel class="filters-expander"
(closed)="panelOpenState.set(false)"> (opened)="panelOpenState.set(true)"
(closed)="panelOpenState.set(false)">
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title>Filters</mat-panel-title> <mat-panel-title>Filters</mat-panel-title>
<mat-panel-description> <mat-panel-description>
@ -11,40 +14,55 @@
</mat-expansion-panel-header> </mat-expansion-panel-header>
<div class="filters"> <div class="filters">
<twitch-redemption-dropdown [(twitchRedemptionId)]="filter_redemption" [search]="true" /> <twitch-redemption-dropdown [(twitchRedemptionId)]="filter_redemption"
<action-dropdown [(action)]="filter_action_name" [search]="true" /> [search]="true" />
<action-dropdown [(action)]="filter_action_name"
[search]="true" />
</div> </div>
</mat-expansion-panel> </mat-expansion-panel>
<div class="table-container"> <div class="table-container">
<table mat-table [dataSource]="redemptions" class="mat-elevation-z8"> <table mat-table
[dataSource]="redemptions"
class="mat-elevation-z8">
<ng-container matColumnDef="twitch-redemption"> <ng-container matColumnDef="twitch-redemption">
<th mat-header-cell *matHeaderCellDef>Twitch Redemption Name</th> <th mat-header-cell
<td mat-cell *matCellDef="let redemption">{{getTwitchRedemptionNameById(redemption.redemption_id) || 'Unknown *matHeaderCellDef>Twitch Redemption Name</th>
<td mat-cell
*matCellDef="let redemption">{{getTwitchRedemptionNameById(redemption.redemption_id) || 'Unknown
Twitch Redemption'}}</td> Twitch Redemption'}}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="action-name"> <ng-container matColumnDef="action-name">
<th mat-header-cell *matHeaderCellDef>Action Name</th> <th mat-header-cell
<td mat-cell *matCellDef="let redemption">{{redemption.action_name}}</td> *matHeaderCellDef>Action Name</th>
<td mat-cell
*matCellDef="let redemption">{{redemption.action_name}}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="order"> <ng-container matColumnDef="order">
<th mat-header-cell *matHeaderCellDef>Order</th> <th mat-header-cell
<td mat-cell *matCellDef="let redemption">{{redemption.order}}</td> *matHeaderCellDef>Order</th>
<td mat-cell
*matCellDef="let redemption">{{redemption.order}}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="misc"> <ng-container matColumnDef="misc">
<th mat-header-cell *matHeaderCellDef></th> <th mat-header-cell
<td mat-cell *matCellDef="let redemption"> *matHeaderCellDef></th>
<button mat-icon-button (click)="openDialog(redemption)"> <td mat-cell
*matCellDef="let redemption">
<button mat-icon-button
(click)="openDialog(redemption)">
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
</button> </button>
</td> </td>
</ng-container> </ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr> <tr mat-header-row
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> *matHeaderRowDef="displayedColumns; sticky: true"></tr>
<tr mat-row
*matRowDef="let row; columns: displayedColumns;"></tr>
</table> </table>
</div> </div>
</div> </div>

View File

@ -10,7 +10,7 @@ describe('RedemptionListComponent', () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [RedemptionListComponent] imports: [RedemptionListComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(RedemptionListComponent); fixture = TestBed.createComponent(RedemptionListComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

View File

@ -10,7 +10,7 @@ describe('RedemptionsComponent', () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [RedemptionsComponent] imports: [RedemptionsComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(RedemptionsComponent); fixture = TestBed.createComponent(RedemptionsComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

View File

@ -18,7 +18,7 @@ export class RedemptionsComponent implements OnInit {
http = inject(HttpClient); http = inject(HttpClient);
route = inject(ActivatedRoute); route = inject(ActivatedRoute);
redemptionService = inject(RedemptionService); redemptionService = inject(RedemptionService);
redemptions: Observable<Redemption[]>|undefined; redemptions: Observable<Redemption[]> | undefined;
ngOnInit(): void { ngOnInit(): void {

View File

@ -1,8 +1,16 @@
<mat-form-field> <mat-form-field>
<mat-label>Twitch Redemption</mat-label> <mat-label>Twitch Redemption</mat-label>
<input type="text" matInput placeholder="Pick a Twitch redemption" aria-label="twitch redemption" <input type="text"
[formControl]="formControl" [matAutocomplete]="auto" (blur)="blur()" (input)="input()"> matInput
<mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn" (optionSelected)="select($event.option.value)"> placeholder="Pick a Twitch redemption"
aria-label="twitch redemption"
[formControl]="formControl"
[matAutocomplete]="auto"
(blur)="blur()"
(input)="input()">
<mat-autocomplete #auto="matAutocomplete"
[displayWith]="displayFn"
(optionSelected)="select($event.option.value)">
@for (redemption of filteredRedemptions; track redemption.id) { @for (redemption of filteredRedemptions; track redemption.id) {
<mat-option [value]="redemption">{{redemption.title}}</mat-option> <mat-option [value]="redemption">{{redemption.title}}</mat-option>
} }

View File

@ -10,7 +10,7 @@ describe('TwitchRedemptionDropdownComponent', () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [TwitchRedemptionDropdownComponent] imports: [TwitchRedemptionDropdownComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(TwitchRedemptionDropdownComponent); fixture = TestBed.createComponent(TwitchRedemptionDropdownComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

View File

@ -3,11 +3,11 @@ import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from
import { ApiAuthenticationService } from '../services/api/api-authentication.service'; import { ApiAuthenticationService } from '../services/api/api-authentication.service';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class AuthAdminGuard implements CanActivate { export class AuthAdminGuard implements CanActivate {
constructor(private auth: ApiAuthenticationService, private router: Router) {} constructor(private auth: ApiAuthenticationService, private router: Router) { }
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> { async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
return this.auth.isAuthenticated() && this.auth.isAdmin(); return this.auth.isAuthenticated() && this.auth.isAdmin();

View File

@ -2,20 +2,20 @@ import { Injectable } from "@angular/core";
import { Subject } from "rxjs" import { Subject } from "rxjs"
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export default class EventService { export default class EventService {
private subject = new Subject(); private subject = new Subject();
emit(eventName: string, payload: any) { emit(eventName: string, payload: any) {
this.subject.next({ eventName, payload }) this.subject.next({ eventName, payload })
} }
listen(eventName: string, callback: (event: any) => void) { listen(eventName: string, callback: (event: any) => void) {
return this.subject.asObservable().subscribe((next: any) => { return this.subject.asObservable().subscribe((next: any) => {
if (eventName == next.eventName) { if (eventName == next.eventName) {
callback(next.payload) callback(next.payload)
} }
}) })
} }
} }

View File

@ -9,6 +9,6 @@ export function createTypeValidator(type: string): ValidatorFn {
return null; return null;
const matches = value.constructor.name === type const matches = value.constructor.name === type
return matches ? null: { invalidType: 'Invalid choice.' }; return matches ? null : { invalidType: 'Invalid choice.' };
} }
} }

View File

@ -6,7 +6,10 @@
<div> <div>
<mat-form-field> <mat-form-field>
<mat-label>Search</mat-label> <mat-label>Search</mat-label>
<input matInput cdkFocusInitial type="text" formControlName="search" /> <input matInput
cdkFocusInitial
type="text"
formControlName="search" />
@if (forms.get('search')?.invalid && (forms.get('search')?.dirty || forms.get('search')?.touched)) { @if (forms.get('search')?.invalid && (forms.get('search')?.dirty || forms.get('search')?.touched)) {
<small class="error">Search is required.</small> <small class="error">Search is required.</small>
} }
@ -15,14 +18,17 @@
<div> <div>
<mat-form-field> <mat-form-field>
<mat-label>Replace</mat-label> <mat-label>Replace</mat-label>
<input matInput formControlName="replace" /> <input matInput
formControlName="replace" />
</mat-form-field> </mat-form-field>
</div> </div>
<div> <div>
<mat-form-field> <mat-form-field>
<mat-label>Regex Options</mat-label> <mat-label>Regex Options</mat-label>
<mat-select multiple [formControl]="flagControl" [compareWith]="compare" <mat-select multiple
(selectionChange)="onSelectionChange($event)"> [formControl]="flagControl"
[compareWith]="compare"
(selectionChange)="onSelectionChange($event)">
<mat-select-trigger> <mat-select-trigger>
{{optionsSelected[0] || ''}} {{optionsSelected[0] || ''}}
@if ((flagControl.value?.length || 0) > 1) { @if ((flagControl.value?.length || 0) > 1) {
@ -43,13 +49,11 @@
</form> </form>
</mat-dialog-content> </mat-dialog-content>
<mat-dialog-actions> <mat-dialog-actions>
<button <button mat-button
mat-button (click)="onCancelClick()">Cancel</button>
(click)="onCancelClick()">Cancel</button> <button mat-button
<button (click)="onSaveClick()"
mat-button [disabled]="!forms.dirty || forms.invalid || waitForResponse">Save</button>
(click)="onSaveClick()"
[disabled]="!forms.dirty || forms.invalid || waitForResponse">Save</button>
</mat-dialog-actions> </mat-dialog-actions>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>

View File

@ -10,7 +10,7 @@ describe('FilterItemEditComponent', () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [FilterItemEditComponent] imports: [FilterItemEditComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(FilterItemEditComponent); fixture = TestBed.createComponent(FilterItemEditComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

View File

@ -11,11 +11,15 @@
</li> </li>
<li> <li>
<mat-menu #filterMenu> <mat-menu #filterMenu>
<button mat-menu-item (click)="openDialog()">Edit</button> <button mat-menu-item
<button mat-menu-item (click)="onDelete.emit(item)">Delete</button> (click)="openDialog()">Edit</button>
<button mat-menu-item
(click)="onDelete.emit(item)">Delete</button>
</mat-menu> </mat-menu>
<button mat-icon-button class="small-button" [matMenuTriggerFor]="filterMenu"> <button mat-icon-button
class="small-button"
[matMenuTriggerFor]="filterMenu">
<mat-icon>more_vert</mat-icon> <mat-icon>more_vert</mat-icon>
</button> </button>
</li> </li>

View File

@ -1,40 +1,40 @@
input { input {
display: inline; display: inline;
font-size: large; font-size: large;
row-gap: 2em; row-gap: 2em;
} }
ul { ul {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: space-evenly; justify-content: space-evenly;
li { li {
list-style-type: none; list-style-type: none;
white-space: pre; white-space: pre;
text-align: start; text-align: start;
text-wrap: wrap; text-wrap: wrap;
span { span {
overflow: hidden; overflow: hidden;
text-overflow: hidden; text-overflow: hidden;
}
> button {
background: #dddddd;
border-radius: 50%;
:hover {
border-radius: 50%;
}
}
} }
li:nth-child(1), >button {
li:nth-child(2) { background: #dddddd;
flex: 1; border-radius: 50%;
:hover {
border-radius: 50%;
}
} }
}
li:nth-child(1),
li:nth-child(2) {
flex: 1;
}
} }
.small-button { .small-button {

View File

@ -11,7 +11,7 @@ describe('FilterItemComponent', () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [FilterItemComponent] imports: [FilterItemComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(FilterItemComponent); fixture = TestBed.createComponent(FilterItemComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

View File

@ -1,17 +1,16 @@
<div> <div>
<ul class="data"> <ul class="data">
<li> <li>
<ul class="header"> <ul class="header">
<li>Search</li> <li>Search</li>
<li>Replace</li> <li>Replace</li>
<li></li> <li></li>
</ul> </ul>
</li> </li>
@for (filter of filters; track $index) { @for (filter of filters; track $index) {
<li> <li>
<tts-filter-item <tts-filter-item [item]="filter"
[item]="filter" (onDelete)="deleteFilter($event)" />
(onDelete)="deleteFilter($event)" />
</li> </li>
} }
</ul> </ul>

View File

@ -7,23 +7,23 @@ ul.data {
padding: 0; padding: 0;
overflow: auto; overflow: auto;
> li { >li {
display: block; display: block;
list-style-type: none; list-style-type: none;
padding: 0.75em 1em; padding: 0.75em 1em;
border-bottom: 1px solid #aaaaaa; border-bottom: 1px solid #aaaaaa;
} }
> li:first-child { >li:first-child {
padding: 0 1em; padding: 0 1em;
border-bottom: 0 solid #aaaaaa; border-bottom: 0 solid #aaaaaa;
} }
> li:last-child { >li:last-child {
border-bottom: 0 solid #aaaaaa; border-bottom: 0 solid #aaaaaa;
} }
> li:nth-child(2n) { >li:nth-child(2n) {
background-color: #f5f5f5; background-color: #f5f5f5;
} }
} }

View File

@ -10,7 +10,7 @@ describe('FilterListComponent', () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [FilterListComponent] imports: [FilterListComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(FilterListComponent); fixture = TestBed.createComponent(FilterListComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

View File

@ -2,13 +2,15 @@
<article> <article>
<h3>Filters</h3> <h3>Filters</h3>
<div> <div>
<button mat-button class="add" aria-label="Add a filter" (click)="openDialog()"> <button mat-button
class="add"
aria-label="Add a filter"
(click)="openDialog()">
<mat-icon>add</mat-icon>Add TTS Filter <mat-icon>add</mat-icon>Add TTS Filter
</button> </button>
</div> </div>
</article> </article>
<div class="grow"> <div class="grow">
<tts-filter-list <tts-filter-list [filters]="items" />
[filters]="items" />
</div> </div>
</main> </main>

View File

@ -10,7 +10,7 @@ describe('FiltersComponent', () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [FiltersComponent] imports: [FiltersComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(FiltersComponent); fixture = TestBed.createComponent(FiltersComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

View File

@ -10,7 +10,7 @@ describe('TwitchAuthCallbackComponent', () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [TwitchAuthCallbackComponent] imports: [TwitchAuthCallbackComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(TwitchAuthCallbackComponent); fixture = TestBed.createComponent(TwitchAuthCallbackComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

View File

@ -1,15 +1,23 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head>
<meta charset="utf-8"> <head>
<title>Tom-to-Speech</title> <meta charset="utf-8">
<base href="/"> <title>Tom-to-Speech</title>
<meta name="viewport" content="width=device-width, initial-scale=1"> <base href="/">
<link rel="icon" type="image/x-icon" href="favicon.ico"> <meta name="viewport"
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet"> content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> <link rel="icon"
</head> type="image/x-icon"
<body class="mat-typography"> href="favicon.ico">
<app-root></app-root> <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap"
</body> rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons"
rel="stylesheet">
</head>
<body class="mat-typography">
<app-root></app-root>
</body>
</html> </html>

View File

@ -1,7 +1,14 @@
/* You can add global styles to this file, and also import other style files */ /* You can add global styles to this file, and also import other style files */
html, body { height: 100%; } html,
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } body {
height: 100%;
}
body {
margin: 0;
font-family: Roboto, "Helvetica Neue", sans-serif;
}
.mat-mdc-dialog-surface { .mat-mdc-dialog-surface {
background-color: transparent !important; background-color: transparent !important;

View File

@ -1,19 +1,19 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{ {
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"compilerOptions": { "compilerOptions": {
"outDir": "./out-tsc/app", "outDir": "./out-tsc/app",
"types": [ "types": [
"node" "node"
]
},
"files": [
"src/main.ts",
"src/main.server.ts",
"server.ts"
],
"include": [
"src/**/*.d.ts"
] ]
},
"files": [
"src/main.ts",
"src/main.server.ts",
"server.ts"
],
"include": [
"src/**/*.d.ts"
]
} }

View File

@ -1,33 +1,33 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{ {
"compileOnSave": false, "compileOnSave": false,
"compilerOptions": { "compilerOptions": {
"outDir": "./dist/out-tsc", "outDir": "./dist/out-tsc",
"strict": true, "strict": true,
"noImplicitOverride": true, "noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true, "noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true, "noImplicitReturns": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"skipLibCheck": true, "skipLibCheck": true,
"esModuleInterop": true, "esModuleInterop": true,
"sourceMap": true, "sourceMap": true,
"declaration": false, "declaration": false,
"experimentalDecorators": true, "experimentalDecorators": true,
"moduleResolution": "bundler", "moduleResolution": "bundler",
"importHelpers": true, "importHelpers": true,
"target": "ES2022", "target": "ES2022",
"module": "ES2022", "module": "ES2022",
"useDefineForClassFields": false, "useDefineForClassFields": false,
"lib": [ "lib": [
"ES2022", "ES2022",
"dom" "dom"
] ]
}, },
"angularCompilerOptions": { "angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false, "enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true, "strictInjectionParameters": true,
"strictInputAccessModifiers": true, "strictInputAccessModifiers": true,
"strictTemplates": true "strictTemplates": true
} }
} }

View File

@ -1,15 +1,15 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{ {
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"compilerOptions": { "compilerOptions": {
"outDir": "./out-tsc/spec", "outDir": "./out-tsc/spec",
"types": [ "types": [
"jasmine" "jasmine"
]
},
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
] ]
},
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
} }