Compare commits
6 Commits
a19a7a0217
...
d0556dce9c
Author | SHA1 | Date | |
---|---|---|---|
d0556dce9c | |||
9201f9b6c5 | |||
74b282ccfd | |||
2f88840ef6 | |||
e949b6df08 | |||
e6f681219c |
3
.gitignore
vendored
3
.gitignore
vendored
@ -41,5 +41,6 @@ testem.log
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
src/environments/*
|
||||
src/environments/
|
||||
src/index.*.html
|
||||
*.code-workspace
|
227
angular.json
227
angular.json
@ -1,115 +1,124 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"hermes-web-angular": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"style": "scss"
|
||||
}
|
||||
},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"options": {
|
||||
"outputPath": "dist/hermes-web-angular",
|
||||
"index": "src/index.html",
|
||||
"browser": "src/main.ts",
|
||||
"polyfills": [
|
||||
"zone.js"
|
||||
],
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"@angular/material/prebuilt-themes/azure-blue.css",
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": [],
|
||||
"server": "src/main.server.ts",
|
||||
"prerender": true,
|
||||
"ssr": {
|
||||
"entry": "server.ts"
|
||||
}
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kB",
|
||||
"maximumError": "1MB"
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"hermes-web-angular": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"style": "scss"
|
||||
}
|
||||
},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"options": {
|
||||
"outputPath": "dist/hermes-web-angular",
|
||||
"index": "src/index.html",
|
||||
"browser": "src/main.ts",
|
||||
"polyfills": [
|
||||
"zone.js"
|
||||
],
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"@angular/material/prebuilt-themes/azure-blue.css",
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": [],
|
||||
"server": "src/main.server.ts",
|
||||
"prerender": true,
|
||||
"ssr": {
|
||||
"entry": "server.ts"
|
||||
}
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "1024kB",
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "2kB",
|
||||
"maximumError": "4kB"
|
||||
"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": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"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": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
28217
package-lock.json
generated
28217
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
98
package.json
98
package.json
@ -1,50 +1,50 @@
|
||||
{
|
||||
"name": "hermes-web-angular",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test",
|
||||
"serve:ssr:hermes-web-angular": "node dist/hermes-web-angular/server/server.mjs"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^19.0.5",
|
||||
"@angular/cdk": "^19.0.4",
|
||||
"@angular/common": "^19.0.5",
|
||||
"@angular/compiler": "^19.0.5",
|
||||
"@angular/core": "^19.0.5",
|
||||
"@angular/forms": "^19.0.5",
|
||||
"@angular/material": "^19.0.4",
|
||||
"@angular/platform-browser": "^19.0.5",
|
||||
"@angular/platform-browser-dynamic": "^19.0.5",
|
||||
"@angular/platform-server": "^19.0.5",
|
||||
"@angular/router": "^19.0.5",
|
||||
"@angular/ssr": "^19.0.6",
|
||||
"angular-oauth2-oidc": "^17.0.2",
|
||||
"express": "^4.18.2",
|
||||
"ngx-socket-io": "^4.7.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"rxjs-websockets": "^9.0.0",
|
||||
"tslib": "^2.3.0",
|
||||
"uuidv4": "^6.2.13",
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^19.0.6",
|
||||
"@angular/cli": "^19.0.6",
|
||||
"@angular/compiler-cli": "^19.0.5",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"@types/node": "^18.18.0",
|
||||
"jasmine-core": "~5.1.0",
|
||||
"karma": "~6.4.0",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
"karma-coverage": "~2.2.0",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"typescript": "~5.6.3"
|
||||
}
|
||||
}
|
||||
"name": "hermes-web-angular",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve -c production --host 0.0.0.0 --watch false",
|
||||
"build": "ng build",
|
||||
"watch": "ng serve -c development --host 0.0.0.0",
|
||||
"test": "ng test",
|
||||
"serve:ssr:hermes-web-angular": "node dist/hermes-web-angular/server/server.mjs"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^19.0.5",
|
||||
"@angular/cdk": "^19.0.4",
|
||||
"@angular/common": "^19.0.5",
|
||||
"@angular/compiler": "^19.0.5",
|
||||
"@angular/core": "^19.0.5",
|
||||
"@angular/forms": "^19.0.5",
|
||||
"@angular/material": "^19.0.4",
|
||||
"@angular/platform-browser": "^19.0.5",
|
||||
"@angular/platform-browser-dynamic": "^19.0.5",
|
||||
"@angular/platform-server": "^19.0.5",
|
||||
"@angular/router": "^19.0.5",
|
||||
"@angular/ssr": "^19.0.6",
|
||||
"angular-oauth2-oidc": "^17.0.2",
|
||||
"express": "^4.18.2",
|
||||
"ngx-socket-io": "^4.7.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"rxjs-websockets": "^9.0.0",
|
||||
"tslib": "^2.3.0",
|
||||
"uuidv4": "^6.2.13",
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^19.0.6",
|
||||
"@angular/cli": "^19.0.6",
|
||||
"@angular/compiler-cli": "^19.0.5",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"@types/node": "^18.18.0",
|
||||
"jasmine-core": "~5.1.0",
|
||||
"karma": "~6.4.0",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
"karma-coverage": "~2.2.0",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"typescript": "~5.6.3"
|
||||
}
|
||||
}
|
@ -1,15 +1,16 @@
|
||||
<mat-form-field>
|
||||
<mat-label>Redeemable Action</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
placeholder="Pick a Redeemable Action"
|
||||
aria-label="redeemable action"
|
||||
[formControl]="formControl"
|
||||
[matAutocomplete]="auto"
|
||||
(blur)="blur()"
|
||||
(input)="input()">
|
||||
<mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn" (optionSelected)="select($event.option.value)">
|
||||
<input matInput
|
||||
type="text"
|
||||
placeholder="Pick a Redeemable Action"
|
||||
aria-label="redeemable action"
|
||||
[formControl]="formControl"
|
||||
[matAutocomplete]="auto"
|
||||
(blur)="blur()"
|
||||
(input)="input()">
|
||||
<mat-autocomplete #auto="matAutocomplete"
|
||||
[displayWith]="displayFn"
|
||||
(optionSelected)="select($event.option.value)">
|
||||
@for (action of filteredActions; track action.name) {
|
||||
<mat-option [value]="action">{{action.name}}</mat-option>
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ describe('ActionDropdownComponent', () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ActionDropdownComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ActionDropdownComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
@ -4,7 +4,7 @@ import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import RedeemableAction from '../../shared/models/redeemable_action';
|
||||
import RedeemableAction from '../../shared/models/redeemable-action';
|
||||
|
||||
@Component({
|
||||
selector: 'action-dropdown',
|
||||
@ -75,7 +75,7 @@ export class ActionDropdownComponent implements OnInit {
|
||||
} else if (insenstiveActions.length == 1) {
|
||||
newValue = insenstiveActions[0];
|
||||
}
|
||||
|
||||
|
||||
if (newValue && newValue.name != this.formControl.value) {
|
||||
this.formControl.setValue(newValue);
|
||||
this.actionChange.emit(newValue.name);
|
||||
|
@ -8,11 +8,14 @@
|
||||
</mat-card-header>
|
||||
|
||||
<mat-card-content>
|
||||
<form class="grid" [formGroup]="formGroup">
|
||||
<form class="grid"
|
||||
[formGroup]="formGroup">
|
||||
<div class="item">
|
||||
<mat-form-field>
|
||||
<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 ||
|
||||
formGroup.get('name')?.touched)) {
|
||||
@if (formGroup.get('name')?.hasError('required')) {
|
||||
@ -27,7 +30,9 @@
|
||||
<div class="item">
|
||||
<mat-form-field>
|
||||
<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) {
|
||||
<mat-option value="{{type}}">{{type}}</mat-option>
|
||||
}
|
||||
@ -50,8 +55,11 @@
|
||||
@if (field.type == 'text') {
|
||||
<mat-form-field>
|
||||
<mat-label>{{field.label}}</mat-label>
|
||||
<input matInput type="text" placeholder="{{field.placeholder}}" [formControl]="field.control"
|
||||
[(ngModel)]="action.data[field.key]">
|
||||
<input matInput
|
||||
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.hasError('required')) {
|
||||
<small class="error">This field is required.</small>
|
||||
@ -65,7 +73,9 @@
|
||||
@else if (field.type == 'number') {
|
||||
<mat-form-field>
|
||||
<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.hasError('required')) {
|
||||
<small class="error">This field is required.</small>
|
||||
@ -77,19 +87,19 @@
|
||||
</mat-form-field>
|
||||
}
|
||||
@else if (field.type == 'text-values') {
|
||||
<mat-form-field>
|
||||
<mat-label>{{field.label}}</mat-label>
|
||||
<mat-select [formControl]="field.control">
|
||||
@for (value of field.values; track $index) {
|
||||
<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-form-field>
|
||||
<mat-label>{{field.label}}</mat-label>
|
||||
<mat-select [formControl]="field.control">
|
||||
@for (value of field.values; track $index) {
|
||||
<mat-option [value]="value">{{value}}</mat-option>
|
||||
}
|
||||
}
|
||||
</mat-form-field>
|
||||
</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-form-field>
|
||||
}
|
||||
|
||||
</div>
|
||||
@ -98,17 +108,17 @@
|
||||
}
|
||||
</mat-card-content>
|
||||
|
||||
<mat-card-actions class="actions" align="end">
|
||||
<mat-card-actions class="actions">
|
||||
@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
|
||||
mat-raised-button
|
||||
(click)="dialogRef.close()">Cancel</button>
|
||||
<button
|
||||
mat-raised-button
|
||||
disabled="{{!formsDirty || !formsValidity || waitForResponse}}"
|
||||
(click)="save()">Save</button>
|
||||
<button mat-raised-button
|
||||
(click)="dialogRef.close()">Cancel</button>
|
||||
<button mat-raised-button
|
||||
disabled="{{!formsDirty || !formsValidity || waitForResponse}}"
|
||||
(click)="save()">Save</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
</body>
|
@ -25,6 +25,6 @@
|
||||
color: #ba1a1a;
|
||||
}
|
||||
|
||||
.mdc-button ~ .mdc-button {
|
||||
.mdc-button~.mdc-button {
|
||||
margin-left: 1em;
|
||||
}
|
@ -10,7 +10,7 @@ describe('ActionItemEditComponent', () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ActionItemEditComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ActionItemEditComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Component, inject, OnInit } from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import RedeemableAction from '../../shared/models/redeemable_action';
|
||||
import RedeemableAction from '../../shared/models/redeemable-action';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
@ -235,9 +235,10 @@ export class ActionItemEditComponent implements OnInit {
|
||||
}
|
||||
|
||||
deleteAction(action: RedeemableAction): void {
|
||||
if (this.isNew)
|
||||
if (this.isNew || this.waitForResponse)
|
||||
return;
|
||||
|
||||
this.waitForResponse = true;
|
||||
this.client.first((d: any) => d.op == 4 && d.d.request.type == 'delete_redeemable_action' && d.d.request.data.name == this.action.name)
|
||||
.subscribe({
|
||||
next: () => this.dialogRef.close(),
|
||||
@ -248,12 +249,15 @@ export class ActionItemEditComponent implements OnInit {
|
||||
}
|
||||
|
||||
save(): void {
|
||||
if (this.formGroup.invalid) {
|
||||
if (this.formGroup.invalid || this.waitForResponse) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.waitForResponse = true;
|
||||
|
||||
const fields = this.actionEntries[this.action.type];
|
||||
if (fields.some(f => f.control.invalid)) {
|
||||
this.waitForResponse = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@ -265,6 +269,7 @@ export class ActionItemEditComponent implements OnInit {
|
||||
}
|
||||
|
||||
if (!(this.action.type in this.actionEntries)) {
|
||||
this.waitForResponse = false;
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,15 @@
|
||||
<main>
|
||||
@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="subtitle">{{action.type}}</span>
|
||||
</button>
|
||||
}
|
||||
<button type="button" class="container" (click)="create()">
|
||||
<button type="button"
|
||||
class="container"
|
||||
(click)="create()">
|
||||
<mat-icon>add</mat-icon>
|
||||
</button>
|
||||
</main>
|
@ -7,7 +7,6 @@ main {
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
justify-self: center;
|
||||
background-color: #fafafa;
|
||||
width: 80%;
|
||||
|
||||
& .container {
|
||||
|
@ -10,7 +10,7 @@ describe('ActionListComponent', () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ActionListComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ActionListComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Component, EventEmitter, inject, Input, Output } from '@angular/core';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import RedeemableAction from '../../shared/models/redeemable_action';
|
||||
import RedeemableAction from '../../shared/models/redeemable-action';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ActionsComponent } from './actions/actions.component';
|
||||
import { ActionListComponent } from './action-list/action-list.component';
|
||||
import { ActionItemEditComponent } from './action-item-edit/action-item-edit.component';
|
||||
|
@ -5,7 +5,8 @@
|
||||
<article>
|
||||
<mat-form-field>
|
||||
<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-icon matPrefix>filter_list</mat-icon> {{filter.name}}
|
||||
</mat-select-trigger>
|
||||
@ -19,13 +20,15 @@
|
||||
<mat-form-field>
|
||||
<mat-label>Search</mat-label>
|
||||
<input matInput
|
||||
type="text"
|
||||
placeholder="Name of action"
|
||||
[formControl]="searchControl"
|
||||
[(ngModel)]="search">
|
||||
type="text"
|
||||
placeholder="Name of action"
|
||||
[formControl]="searchControl"
|
||||
[(ngModel)]="search">
|
||||
<mat-icon matPrefix>search</mat-icon>
|
||||
</mat-form-field>
|
||||
</article>
|
||||
</section>
|
||||
<action-list [actions]="actions" (actionsChange)="items.push($event)" />
|
||||
<action-list class="center"
|
||||
[actions]="actions"
|
||||
(actionsChange)="items.push($event)" />
|
||||
</body>
|
@ -1,5 +1,5 @@
|
||||
body, h3 {
|
||||
background-color: #fafafa;
|
||||
body,
|
||||
h3 {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
@ -12,6 +12,7 @@ body {
|
||||
|
||||
h3 {
|
||||
margin-bottom: 2em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
section {
|
||||
@ -21,13 +22,18 @@ section {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
@media (max-width:1250px) {
|
||||
@media (max-width:1250px) {
|
||||
display: block;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
article {
|
||||
display: flex;
|
||||
justify-content:space-around;
|
||||
justify-content: space-around;
|
||||
}
|
||||
}
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
@ -10,7 +10,7 @@ describe('ActionsComponent', () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ActionsComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ActionsComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
@ -5,10 +5,10 @@ import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { HermesClientService } from '../../hermes-client.service';
|
||||
import RedeemableAction from '../../shared/models/redeemable_action';
|
||||
import RedeemableAction from '../../shared/models/redeemable-action';
|
||||
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { RedeemableActionService } from '../../shared/services/redeemable-action.service';
|
||||
import RedeemableActionService from '../../shared/services/redeemable-action.service';
|
||||
|
||||
interface IActionFilter {
|
||||
name: string
|
||||
|
@ -1,4 +1,4 @@
|
||||
<main class="main">
|
||||
<navigation class="navigation" />
|
||||
<router-outlet class="content" />
|
||||
<navigation class="navigation" />
|
||||
<router-outlet class="content" />
|
||||
</main>
|
@ -1,4 +1,4 @@
|
||||
.main {
|
||||
display: grid;
|
||||
grid-template-columns: 20em 0px 1fr;
|
||||
display: grid;
|
||||
grid-template-columns: 20em 0px 1fr;
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { isPlatformBrowser } from '@angular/common';
|
||||
import { Component, OnInit, Inject, PLATFORM_ID, NgZone, OnDestroy, inject } from '@angular/core';
|
||||
import { Router, RouterOutlet } from '@angular/router';
|
||||
import { ActivatedRoute, Router, RouterOutlet } from '@angular/router';
|
||||
import { HermesClientService } from './hermes-client.service';
|
||||
import { AuthUserGuard } from './shared/auth/auth.user.guard'
|
||||
import { first, Subscription, timeout } from 'rxjs';
|
||||
|
@ -12,9 +12,9 @@ export const appConfig: ApplicationConfig = {
|
||||
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||
provideRouter(routes),
|
||||
provideHttpClient(
|
||||
withInterceptors([(req: HttpRequest<unknown>, next: HttpHandlerFn) => {
|
||||
return next(req);
|
||||
}])
|
||||
withInterceptors([(req: HttpRequest<unknown>, next: HttpHandlerFn) => {
|
||||
return next(req);
|
||||
}])
|
||||
),
|
||||
provideOAuthClient(),
|
||||
provideClientHydration(), provideAnimationsAsync()
|
||||
|
@ -14,12 +14,42 @@ import TwitchRedemptionResolver from './shared/resolvers/twitch-redemption-resol
|
||||
import RedeemableActionResolver from './shared/resolvers/redeemable-action-resolver';
|
||||
import TtsFilterResolver from './shared/resolvers/tts-filter-resolver';
|
||||
import ApiKeyResolver from './shared/resolvers/api-key-resolver';
|
||||
import GroupResolver from './shared/resolvers/group-resolver';
|
||||
import PolicyResolver from './shared/resolvers/policy-resolver';
|
||||
import { GroupsComponent } from './groups/groups/groups.component';
|
||||
import { GroupPageComponent } from './groups/group-page/group-page.component';
|
||||
import GroupChatterResolver from './shared/resolvers/group-chatter-resolver';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: 'policies',
|
||||
component: PolicyComponent,
|
||||
canActivate: [AuthUserGuard],
|
||||
resolve: {
|
||||
groups: GroupResolver,
|
||||
chatters: GroupChatterResolver,
|
||||
policies: PolicyResolver,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'groups',
|
||||
component: GroupsComponent,
|
||||
canActivate: [AuthAdminGuard],
|
||||
resolve: {
|
||||
groups: GroupResolver,
|
||||
chatters: GroupChatterResolver,
|
||||
policies: PolicyResolver,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'groups/:id',
|
||||
component: GroupPageComponent,
|
||||
canActivate: [AuthAdminGuard],
|
||||
resolve: {
|
||||
groups: GroupResolver,
|
||||
chatters: GroupChatterResolver,
|
||||
policies: PolicyResolver,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'filters',
|
||||
|
@ -2,7 +2,8 @@
|
||||
<main>
|
||||
<mat-form-field>
|
||||
<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>
|
||||
@for (user of users; track user.id) {
|
||||
<mat-option [value]="user.id">{{ user.name }}</mat-option>
|
||||
|
@ -10,7 +10,7 @@ describe('ImpersonationComponent', () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ImpersonationComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ImpersonationComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, Inject, OnInit, PLATFORM_ID } from '@angular/core';
|
||||
import { Component, inject, Inject, OnInit, PLATFORM_ID } from '@angular/core';
|
||||
import { ApiAuthenticationService } from '../../shared/services/api/api-authentication.service';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
@ -8,6 +8,9 @@ import { environment } from '../../../environments/environment';
|
||||
import EventService from '../../shared/services/EventService';
|
||||
import { HermesClientService } from '../../hermes-client.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { timeout, first } from 'rxjs';
|
||||
import ApiKey from '../../shared/models/api-key';
|
||||
import { ApiKeyService } from '../../shared/services/api/api-key.service';
|
||||
|
||||
@Component({
|
||||
selector: 'impersonation',
|
||||
@ -17,6 +20,8 @@ import { Router } from '@angular/router';
|
||||
styleUrl: './impersonation.component.scss'
|
||||
})
|
||||
export class ImpersonationComponent implements OnInit {
|
||||
private readonly keyService = inject(ApiKeyService);
|
||||
|
||||
impersonated: string | undefined;
|
||||
users: { id: string, name: string }[];
|
||||
|
||||
@ -25,7 +30,7 @@ export class ImpersonationComponent implements OnInit {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (!isPlatformBrowser(this.platformId)) {
|
||||
if (!isPlatformBrowser(this.platformId) || !this.auth.isAdmin()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -40,6 +45,18 @@ export class ImpersonationComponent implements OnInit {
|
||||
this.impersonated = id;
|
||||
}
|
||||
});
|
||||
|
||||
if (this.auth.isAdmin()) {
|
||||
this.events.listen('impersonation', (userId) => {
|
||||
this.keyService.fetch(true)
|
||||
.pipe(timeout(3000), first())
|
||||
.subscribe(async (d: ApiKey[]) => {
|
||||
if (d.length > 0)
|
||||
this.hermes.login(d[0].id);
|
||||
await this.router.navigate([this.router.url.substring(1)]);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public isAdmin() {
|
||||
@ -51,6 +68,9 @@ export class ImpersonationComponent implements OnInit {
|
||||
}
|
||||
|
||||
public onChange(e: any) {
|
||||
if (!this.auth.isAdmin())
|
||||
return;
|
||||
|
||||
if (!e.value) {
|
||||
this.http.delete(environment.API_HOST + '/admin/impersonate', {
|
||||
headers: {
|
||||
@ -62,7 +82,6 @@ export class ImpersonationComponent implements OnInit {
|
||||
}).subscribe(async (data: any) => {
|
||||
this.hermes.disconnect();
|
||||
this.events.emit('impersonation', e.value);
|
||||
await this.router.navigate(['tts-login']);
|
||||
});
|
||||
} else {
|
||||
this.http.put(environment.API_HOST + '/admin/impersonate', {
|
||||
@ -78,4 +97,4 @@ export class ImpersonationComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
<div class="login">
|
||||
<mat-card class="outer" appearance="outlined">
|
||||
<mat-card class="outer"
|
||||
appearance="outlined">
|
||||
<mat-card-header>
|
||||
<h1 class="title">Login</h1>
|
||||
</mat-card-header>
|
||||
@ -8,7 +9,9 @@
|
||||
<p>Log in with your favorite livestream service</p>
|
||||
|
||||
<a>
|
||||
<mat-card appearance="outlined" class="twitch" (click)="login()">
|
||||
<mat-card appearance="outlined"
|
||||
class="twitch"
|
||||
(click)="login()">
|
||||
<mat-card-content>
|
||||
Twitch
|
||||
</mat-card-content>
|
||||
|
@ -10,7 +10,7 @@ describe('LoginComponent', () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [LoginComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(LoginComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
@ -5,29 +5,29 @@ import { Subscription } from 'rxjs';
|
||||
import { environment } from '../../../environments/environment';
|
||||
|
||||
@Component({
|
||||
selector: 'login',
|
||||
standalone: true,
|
||||
imports: [MatCardModule, RouterModule],
|
||||
templateUrl: './login.component.html',
|
||||
styleUrl: './login.component.scss'
|
||||
selector: 'login',
|
||||
standalone: true,
|
||||
imports: [MatCardModule, RouterModule],
|
||||
templateUrl: './login.component.html',
|
||||
styleUrl: './login.component.scss'
|
||||
})
|
||||
export class LoginComponent implements OnInit, OnDestroy {
|
||||
subscription: Subscription | null;
|
||||
subscription: Subscription | null;
|
||||
|
||||
constructor(private router: Router) {
|
||||
this.subscription = null;
|
||||
}
|
||||
constructor(private router: Router) {
|
||||
this.subscription = null;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
}
|
||||
ngOnInit(): void {
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.subscription)
|
||||
this.subscription.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
login() {
|
||||
document.location.replace(environment.API_HOST + '/auth');
|
||||
}
|
||||
ngOnDestroy(): void {
|
||||
if (this.subscription)
|
||||
this.subscription.unsubscribe()
|
||||
}
|
||||
|
||||
login() {
|
||||
document.location.replace(environment.API_HOST + '/auth');
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,8 @@
|
||||
</mat-form-field>
|
||||
</mat-card-content>
|
||||
<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>
|
||||
</main>
|
@ -10,7 +10,7 @@ describe('TtsLoginComponent', () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [TtsLoginComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(TtsLoginComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
@ -1,6 +1,7 @@
|
||||
@if (auth.isAuthenticated()) {
|
||||
<main>
|
||||
<mat-card appearance="outlined" class="card">
|
||||
<mat-card appearance="outlined"
|
||||
class="card">
|
||||
<mat-card-header>
|
||||
<mat-card-title>{{username}}</mat-card-title>
|
||||
</mat-card-header>
|
||||
@ -10,9 +11,11 @@
|
||||
<mat-card-actions class="actions">
|
||||
<div>
|
||||
@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>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
|
@ -14,10 +14,11 @@ main {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.disconnect, .logoff {
|
||||
.disconnect,
|
||||
.logoff {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.mdc-button ~ .mdc-button {
|
||||
.mdc-button~.mdc-button {
|
||||
margin-left: 1em;
|
||||
}
|
@ -10,7 +10,7 @@ describe('UserCardComponent', () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [UserCardComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(UserCardComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
26
src/app/groups/group-dropdown/group-dropdown.component.html
Normal file
26
src/app/groups/group-dropdown/group-dropdown.component.html
Normal file
@ -0,0 +1,26 @@
|
||||
<mat-form-field>
|
||||
<mat-label>Group</mat-label>
|
||||
<input matInput
|
||||
type="text"
|
||||
placeholder="Pick a group"
|
||||
aria-label="group"
|
||||
[formControl]="formControl"
|
||||
[matAutocomplete]="auto"
|
||||
[readonly]="!!groupDisabled"
|
||||
(blur)="blur()"
|
||||
(input)="input()">
|
||||
<mat-autocomplete #auto="matAutocomplete"
|
||||
[displayWith]="displayFn"
|
||||
(optionSelected)="select($event.option.value)">
|
||||
@for (group of filteredGroups; track group.id) {
|
||||
<mat-option [value]="group">{{group.name}}</mat-option>
|
||||
}
|
||||
</mat-autocomplete>
|
||||
@if (!search && formControl.invalid && (formControl.dirty || formControl.touched)) {
|
||||
@for (error of errorMessageKeys; track $index) {
|
||||
@if (formControl.hasError(error)) {
|
||||
<small class="error">{{errorMessages[error]}}</small>
|
||||
}
|
||||
}
|
||||
}
|
||||
</mat-form-field>
|
@ -0,0 +1,3 @@
|
||||
.error {
|
||||
color: #ba1a1a;
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { GroupDropdownComponent } from './group-dropdown.component';
|
||||
|
||||
describe('GroupDropdownComponent', () => {
|
||||
let component: GroupDropdownComponent;
|
||||
let fixture: ComponentFixture<GroupDropdownComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [GroupDropdownComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(GroupDropdownComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
99
src/app/groups/group-dropdown/group-dropdown.component.ts
Normal file
99
src/app/groups/group-dropdown/group-dropdown.component.ts
Normal file
@ -0,0 +1,99 @@
|
||||
import { Component, EventEmitter, inject, Input, OnInit, Output } from '@angular/core';
|
||||
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Group } from '../../shared/models/group';
|
||||
|
||||
@Component({
|
||||
selector: 'group-dropdown',
|
||||
imports: [
|
||||
MatAutocompleteModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
ReactiveFormsModule,
|
||||
],
|
||||
templateUrl: './group-dropdown.component.html',
|
||||
styleUrl: './group-dropdown.component.scss'
|
||||
})
|
||||
export class GroupDropdownComponent implements OnInit {
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
|
||||
@Input() formControl = new FormControl<Group | string | undefined>(undefined);
|
||||
@Input() errorMessages: { [errorKey: string]: string } = {};
|
||||
@Input({ required: true }) groups: Group[] = [];
|
||||
@Input() group: string | undefined;
|
||||
@Input() groupDisabled: boolean | undefined;
|
||||
@Input() search: boolean = false;
|
||||
@Output() readonly groupChange = new EventEmitter<string>();
|
||||
|
||||
errorMessageKeys: string[] = [];
|
||||
|
||||
|
||||
constructor() {
|
||||
this.route.data.subscribe(data => {
|
||||
if (!data['groups'])
|
||||
return;
|
||||
|
||||
this.groups = data['groups'];
|
||||
});
|
||||
|
||||
if (this.groupDisabled)
|
||||
this.formControl.disable();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.errorMessageKeys = Object.keys(this.errorMessages);
|
||||
|
||||
if (!this.group)
|
||||
return;
|
||||
|
||||
const group = this.groups.find(r => r.id == this.group);
|
||||
this.formControl.setValue(group);
|
||||
}
|
||||
|
||||
get filteredGroups() {
|
||||
const value = this.formControl.value;
|
||||
if (typeof value == 'string') {
|
||||
return this.groups.filter(r => r.name.toLowerCase().includes(value.toLowerCase()));
|
||||
}
|
||||
return this.groups;
|
||||
}
|
||||
|
||||
select(event: Group) {
|
||||
this.groupChange.emit(event.id);
|
||||
}
|
||||
|
||||
input() {
|
||||
if (this.search && typeof this.formControl.value == 'string') {
|
||||
this.groupChange.emit(this.formControl.value);
|
||||
}
|
||||
}
|
||||
|
||||
blur() {
|
||||
if (!this.search && typeof this.formControl.value == 'string') {
|
||||
const name = this.formControl.value;
|
||||
const nameLower = name.toLowerCase();
|
||||
let newValue: Group | undefined = undefined;
|
||||
const insenstiveGroups = this.filteredGroups.filter(a => a.name.toLowerCase() == nameLower);
|
||||
if (insenstiveGroups.length > 1) {
|
||||
const sensitiveGroup = insenstiveGroups.find(a => a.name == name);
|
||||
newValue = sensitiveGroup ?? undefined;
|
||||
} else if (insenstiveGroups.length == 1) {
|
||||
newValue = insenstiveGroups[0];
|
||||
}
|
||||
|
||||
if (newValue) {
|
||||
this.formControl.setValue(newValue);
|
||||
//this.groupChange.emit(newValue.name);
|
||||
} else if (!newValue)
|
||||
this.formControl.setValue(undefined);
|
||||
//this.groupChange.emit(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
displayFn(value: Group) {
|
||||
return value?.name;
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title-group>
|
||||
<mat-card-title>Edit Group</mat-card-title>
|
||||
</mat-card-title-group>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<mat-form-field>
|
||||
<mat-label>Group Name</mat-label>
|
||||
<input matInput
|
||||
type="text"
|
||||
[formControl]="nameForm"
|
||||
[disabled]="isSpecial" />
|
||||
@if (nameForm.invalid && (nameForm.dirty || nameForm.touched)) {
|
||||
@if (nameForm.hasError('required')) {
|
||||
<small class="error">This field is required.</small>
|
||||
}
|
||||
}
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-label>TTS Priority</mat-label>
|
||||
<input matInput
|
||||
type="number"
|
||||
[formControl]="priorityForm" />
|
||||
@if (priorityForm.invalid && (priorityForm.dirty || priorityForm.touched)) {
|
||||
@if (priorityForm.hasError('required')) {
|
||||
<small class="error">This field is required.</small>
|
||||
}
|
||||
@if (priorityForm.hasError('min')) {
|
||||
<small class="error">This field must be greater than -2147483649.</small>
|
||||
}
|
||||
@if (priorityForm.hasError('max')) {
|
||||
<small class="error">This field must be smaller than 2147483648.</small>
|
||||
}
|
||||
@if (priorityForm.hasError('integer') && !priorityForm.hasError('min') && !priorityForm.hasError('max')) {
|
||||
<small class="error">This field must be an integer.</small>
|
||||
}
|
||||
}
|
||||
</mat-form-field>
|
||||
</mat-card-content>
|
||||
<mat-card-actions>
|
||||
<button mat-button
|
||||
[disabled]="waitForResponse || formGroup.invalid"
|
||||
(click)="add()">
|
||||
<mat-icon>add</mat-icon>Add
|
||||
</button>
|
||||
<button mat-button
|
||||
[disabled]="waitForResponse"
|
||||
(click)="cancel()">
|
||||
<mat-icon>cancel</mat-icon>Cancel
|
||||
</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
@ -0,0 +1,8 @@
|
||||
.mat-mdc-form-field {
|
||||
display: block;
|
||||
margin: 1em;
|
||||
}
|
||||
|
||||
.mat-mdc-card-actions {
|
||||
align-self: center;
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { GroupItemEditComponent } from './group-item-edit.component';
|
||||
|
||||
describe('GroupItemEditComponent', () => {
|
||||
let component: GroupItemEditComponent;
|
||||
let fixture: ComponentFixture<GroupItemEditComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [GroupItemEditComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(GroupItemEditComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
67
src/app/groups/group-item-edit/group-item-edit.component.ts
Normal file
67
src/app/groups/group-item-edit/group-item-edit.component.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { Component, inject, Input, OnInit } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { Group } from '../../shared/models/group';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { HermesClientService } from '../../hermes-client.service';
|
||||
import { integerValidator } from '../../shared/validators/integer';
|
||||
|
||||
@Component({
|
||||
selector: 'group-item-edit',
|
||||
imports: [
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatIconModule,
|
||||
ReactiveFormsModule,
|
||||
],
|
||||
templateUrl: './group-item-edit.component.html',
|
||||
styleUrl: './group-item-edit.component.scss'
|
||||
})
|
||||
export class GroupItemEditComponent implements OnInit {
|
||||
private readonly _client = inject(HermesClientService);
|
||||
private readonly _dialogRef = inject(MatDialogRef<GroupItemEditComponent>);
|
||||
private readonly _data = inject(MAT_DIALOG_DATA);
|
||||
|
||||
group: Group = { id: '', user_id: '', name: '', priority: 0 };
|
||||
isSpecial: boolean = false;
|
||||
waitForResponse: boolean = false;
|
||||
|
||||
nameForm = new FormControl('', [Validators.required]);
|
||||
priorityForm = new FormControl(0, [Validators.required, Validators.min(-2147483648), Validators.max(2147483647), integerValidator]);
|
||||
formGroup = new FormGroup({
|
||||
name: this.nameForm,
|
||||
priority: this.priorityForm,
|
||||
});
|
||||
|
||||
ngOnInit() {
|
||||
this.group = this._data.group;
|
||||
this.isSpecial = this._data.isSpecial;
|
||||
this.nameForm.setValue(this.group.name);
|
||||
if (this.isSpecial)
|
||||
this.nameForm.disable();
|
||||
this.priorityForm.setValue(this.group.priority);
|
||||
}
|
||||
|
||||
add() {
|
||||
if (this.formGroup.invalid || this.waitForResponse)
|
||||
return;
|
||||
|
||||
this._client.first((d: any) => d.op == 4 && d.d.request.type == 'create_group' && d.d.data.name == this.nameForm.value)
|
||||
.subscribe({
|
||||
next: (d) => this._dialogRef.close(d.d.data),
|
||||
error: () => this.waitForResponse = false,
|
||||
complete: () => this.waitForResponse = false,
|
||||
});
|
||||
this._client.createGroup(this.nameForm.value!, this.priorityForm.value!);
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this._dialogRef.close();
|
||||
}
|
||||
}
|
22
src/app/groups/group-item/group-item.component.html
Normal file
22
src/app/groups/group-item/group-item.component.html
Normal file
@ -0,0 +1,22 @@
|
||||
<article>
|
||||
<section class="title">{{item().group.name}}
|
||||
@if (special) {
|
||||
<small class="tag">auto-generated</small>
|
||||
}
|
||||
</section>
|
||||
<section class="">{{item().group.priority}}</section>
|
||||
<section>
|
||||
{{item().chatters.length}}
|
||||
<small class="muted block">user{{item().chatters.length == 1 ? '' : 's'}}</small>
|
||||
</section>
|
||||
<section>
|
||||
{{item().policies.length}}
|
||||
<small class="muted block">polic{{item().chatters.length == 1 ? 'y' : 'ies'}}</small>
|
||||
</section>
|
||||
<section>
|
||||
<button mat-button
|
||||
(click)="router.navigate([link])">
|
||||
<mat-icon>pageview</mat-icon>View
|
||||
</button>
|
||||
</section>
|
||||
</article>
|
45
src/app/groups/group-item/group-item.component.scss
Normal file
45
src/app/groups/group-item/group-item.component.scss
Normal file
@ -0,0 +1,45 @@
|
||||
article {
|
||||
background-color: #f0f0f0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
border-radius: 15px;
|
||||
padding: 1em;
|
||||
|
||||
& :first-child {
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
& :not(:first-child) {
|
||||
text-align: center;
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.5em;
|
||||
word-break: keep-all;
|
||||
}
|
||||
|
||||
section {
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.block {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tag {
|
||||
font-size: 11px;
|
||||
background-color: white;
|
||||
color: rgb(204, 51, 204);
|
||||
padding: 4px;
|
||||
margin: 0 5px;
|
||||
border-radius: 10px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: grey;
|
||||
margin: 5px 0;
|
||||
}
|
23
src/app/groups/group-item/group-item.component.spec.ts
Normal file
23
src/app/groups/group-item/group-item.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { GroupItemComponent } from './group-item.component';
|
||||
|
||||
describe('GroupItemComponent', () => {
|
||||
let component: GroupItemComponent;
|
||||
let fixture: ComponentFixture<GroupItemComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [GroupItemComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(GroupItemComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
35
src/app/groups/group-item/group-item.component.ts
Normal file
35
src/app/groups/group-item/group-item.component.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { Component, inject, input, Input, OnInit } from '@angular/core';
|
||||
import { Group } from '../../shared/models/group';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { Policy } from '../../shared/models/policy';
|
||||
import { GroupItemEditComponent } from '../group-item-edit/group-item-edit.component';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { Router } from '@angular/router';
|
||||
import { GroupChatter } from '../../shared/models/group-chatter';
|
||||
|
||||
@Component({
|
||||
selector: 'group-item',
|
||||
standalone: true,
|
||||
imports: [
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
MatIconModule,
|
||||
],
|
||||
templateUrl: './group-item.component.html',
|
||||
styleUrl: './group-item.component.scss'
|
||||
})
|
||||
export class GroupItemComponent implements OnInit {
|
||||
readonly router = inject(Router);
|
||||
item = input.required<{ group: Group, chatters: GroupChatter[], policies: Policy[] }>();
|
||||
link: string = '';
|
||||
|
||||
|
||||
special: boolean = true;
|
||||
|
||||
ngOnInit() {
|
||||
this.special = ['everyone', 'subscribers', 'moderators', 'vip', 'broadcaster'].includes(this.item().group.name);
|
||||
this.link = 'groups/' + this.item().group.id;
|
||||
}
|
||||
}
|
7
src/app/groups/group-list/group-list.component.html
Normal file
7
src/app/groups/group-list/group-list.component.html
Normal file
@ -0,0 +1,7 @@
|
||||
<ul>
|
||||
@for (group of groups; track $index) {
|
||||
<li>
|
||||
<group-item [item]="group" />
|
||||
</li>
|
||||
}
|
||||
</ul>
|
9
src/app/groups/group-list/group-list.component.scss
Normal file
9
src/app/groups/group-list/group-list.component.scss
Normal file
@ -0,0 +1,9 @@
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
list-style: none;
|
||||
margin: 1em;
|
||||
}
|
23
src/app/groups/group-list/group-list.component.spec.ts
Normal file
23
src/app/groups/group-list/group-list.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { GroupListComponent } from './group-list.component';
|
||||
|
||||
describe('GroupListComponent', () => {
|
||||
let component: GroupListComponent;
|
||||
let fixture: ComponentFixture<GroupListComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [GroupListComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(GroupListComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
36
src/app/groups/group-list/group-list.component.ts
Normal file
36
src/app/groups/group-list/group-list.component.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { Group } from '../../shared/models/group';
|
||||
import { GroupItemComponent } from "../group-item/group-item.component";
|
||||
import { Policy } from '../../shared/models/policy';
|
||||
import { GroupChatter } from '../../shared/models/group-chatter';
|
||||
|
||||
@Component({
|
||||
selector: 'group-list',
|
||||
standalone: true,
|
||||
imports: [GroupItemComponent],
|
||||
templateUrl: './group-list.component.html',
|
||||
styleUrl: './group-list.component.scss'
|
||||
})
|
||||
export class GroupListComponent {
|
||||
private _groups: { group: Group, chatters: GroupChatter[], policies: Policy[] }[] = [];
|
||||
private _filter: (item: { group: Group, chatters: GroupChatter[], policies: Policy[] }) => boolean = _ => true;
|
||||
|
||||
|
||||
get filter(): (item: { group: Group, chatters: GroupChatter[], policies: Policy[] }) => boolean {
|
||||
return this._filter;
|
||||
}
|
||||
|
||||
@Input({ alias: 'filter', required: false })
|
||||
set filter(value: (item: { group: Group, chatters: GroupChatter[], policies: Policy[] }) => boolean) {
|
||||
this._filter = value;
|
||||
}
|
||||
|
||||
get groups() {
|
||||
return this._groups.filter(this._filter);
|
||||
}
|
||||
|
||||
@Input({ alias: 'groups', required: true })
|
||||
set groups(value: { group: Group, chatters: GroupChatter[], policies: Policy[] }[]) {
|
||||
this._groups = value;
|
||||
}
|
||||
}
|
43
src/app/groups/group-page/group-page.component.html
Normal file
43
src/app/groups/group-page/group-page.component.html
Normal file
@ -0,0 +1,43 @@
|
||||
<div>
|
||||
<h2>{{group?.name}}</h2>
|
||||
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>Policies</mat-panel-title>
|
||||
<mat-panel-description class="muted">
|
||||
{{policies.length}} polic{{policies.length == 1 ? 'y' : 'ies'}}
|
||||
</mat-panel-description>
|
||||
</mat-expansion-panel-header>
|
||||
<policy-add-button class="add"
|
||||
[groups]="groups"
|
||||
[policies]="policies"
|
||||
[group]="group?.id" />
|
||||
@if (policies.length > 0) {
|
||||
<policy-table [policies]="policies" />
|
||||
}
|
||||
</mat-expansion-panel>
|
||||
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title class="danger">Danger Zone</mat-panel-title>
|
||||
<mat-panel-description class="muted">
|
||||
Dangerous actions
|
||||
</mat-panel-description>
|
||||
</mat-expansion-panel-header>
|
||||
<div class="content">
|
||||
<section>
|
||||
<article class="left">
|
||||
<h4>Deletion</h4>
|
||||
<p>Deleting this group will delete everything that is part of it, including policies and permissions.</p>
|
||||
</article>
|
||||
<article class="right">
|
||||
<button mat-raised-button
|
||||
class="delete"
|
||||
(click)="delete()">
|
||||
<mat-icon>delete</mat-icon>Delete this group.
|
||||
</button>
|
||||
</article>
|
||||
</section>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
</div>
|
21
src/app/groups/group-page/group-page.component.scss
Normal file
21
src/app/groups/group-page/group-page.component.scss
Normal file
@ -0,0 +1,21 @@
|
||||
.mat-expansion-panel~.mat-expansion-panel {
|
||||
margin-top: 4em;
|
||||
}
|
||||
|
||||
.delete {
|
||||
justify-content: space-around;
|
||||
color: red;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: grey;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.left {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.right {
|
||||
float: right;
|
||||
}
|
23
src/app/groups/group-page/group-page.component.spec.ts
Normal file
23
src/app/groups/group-page/group-page.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { GroupPageComponent } from './group-page.component';
|
||||
|
||||
describe('GroupPageComponent', () => {
|
||||
let component: GroupPageComponent;
|
||||
let fixture: ComponentFixture<GroupPageComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [GroupPageComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(GroupPageComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
86
src/app/groups/group-page/group-page.component.ts
Normal file
86
src/app/groups/group-page/group-page.component.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Group } from '../../shared/models/group';
|
||||
import { Policy } from '../../shared/models/policy';
|
||||
import { MatExpansionModule } from '@angular/material/expansion';
|
||||
import { PoliciesModule } from '../../policies/policies.module';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { PolicyTableComponent } from "../../policies/policy-table/policy-table.component";
|
||||
import { PolicyAddButtonComponent } from '../../policies/policy-add-button/policy-add-button.component';
|
||||
import { HermesClientService } from '../../hermes-client.service';
|
||||
import { GroupChatter } from '../../shared/models/group-chatter';
|
||||
|
||||
@Component({
|
||||
selector: 'group-page',
|
||||
imports: [
|
||||
MatButtonModule,
|
||||
MatExpansionModule,
|
||||
MatFormFieldModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
PoliciesModule,
|
||||
PolicyAddButtonComponent,
|
||||
ReactiveFormsModule,
|
||||
PolicyTableComponent
|
||||
],
|
||||
templateUrl: './group-page.component.html',
|
||||
styleUrl: './group-page.component.scss'
|
||||
})
|
||||
export class GroupPageComponent {
|
||||
private readonly _router = inject(Router);
|
||||
private readonly _route = inject(ActivatedRoute);
|
||||
private readonly _client = inject(HermesClientService);
|
||||
private _group: Group | undefined;
|
||||
private _chatters: GroupChatter[];
|
||||
private _policies: Policy[];
|
||||
|
||||
groups: Group[] = [];
|
||||
|
||||
constructor() {
|
||||
this._chatters = [];
|
||||
this._policies = [];
|
||||
|
||||
this._route.params.subscribe((p: any) => {
|
||||
const group_id = p.id;
|
||||
|
||||
this._route.data.subscribe(async (data: any) => {
|
||||
this.groups = [...data['groups']];
|
||||
const group = this.groups.find((g: Group) => g.id == group_id);
|
||||
|
||||
if (!group) {
|
||||
await this._router.navigate(['groups']);
|
||||
return;
|
||||
}
|
||||
|
||||
this._group = group;
|
||||
this._chatters = [...data['chatters'].filter((c: GroupChatter) => c.group_id == group_id)];
|
||||
this._policies = [...data['policies'].filter((p: Policy) => p.group_id == group_id)];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
get group() {
|
||||
return this._group;
|
||||
}
|
||||
|
||||
get chatters() {
|
||||
return this._chatters;
|
||||
}
|
||||
|
||||
get policies() {
|
||||
return this._policies;
|
||||
}
|
||||
|
||||
delete() {
|
||||
if (!this.group)
|
||||
return;
|
||||
|
||||
this._client.first(d => d.d.request.type == 'delete_group' && d.d.request.data.id == this.group!.id)
|
||||
.subscribe(async () => await this._router.navigate(['groups']));
|
||||
this._client.deleteGroup(this.group.id);
|
||||
}
|
||||
}
|
18
src/app/groups/groups.module.ts
Normal file
18
src/app/groups/groups.module.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { GroupDropdownComponent } from './group-dropdown/group-dropdown.component';
|
||||
import { GroupsComponent } from './groups/groups.component';
|
||||
import { GroupListComponent } from './group-list/group-list.component';
|
||||
import { GroupItemComponent } from './group-item/group-item.component';
|
||||
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
imports: [
|
||||
GroupDropdownComponent,
|
||||
GroupListComponent,
|
||||
GroupItemComponent,
|
||||
GroupsComponent,
|
||||
]
|
||||
})
|
||||
export class GroupsModule { }
|
21
src/app/groups/groups/groups.component.html
Normal file
21
src/app/groups/groups/groups.component.html
Normal file
@ -0,0 +1,21 @@
|
||||
<button mat-button
|
||||
[mat-menu-trigger-for]="menu">
|
||||
<mat-icon>add</mat-icon>
|
||||
Add a group
|
||||
</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
<button mat-menu-item
|
||||
(click)="openDialog('')">Custom Group</button>
|
||||
<button mat-menu-item
|
||||
(click)="openDialog('everyone')">Everyone Group</button>
|
||||
<button mat-menu-item
|
||||
(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>
|
||||
<group-list class="groups"
|
||||
[groups]="items" />
|
7
src/app/groups/groups/groups.component.scss
Normal file
7
src/app/groups/groups/groups.component.scss
Normal file
@ -0,0 +1,7 @@
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.delete {
|
||||
color: red;
|
||||
}
|
23
src/app/groups/groups/groups.component.spec.ts
Normal file
23
src/app/groups/groups/groups.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { GroupsComponent } from './groups.component';
|
||||
|
||||
describe('GroupsComponent', () => {
|
||||
let component: GroupsComponent;
|
||||
let fixture: ComponentFixture<GroupsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [GroupsComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(GroupsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
119
src/app/groups/groups/groups.component.ts
Normal file
119
src/app/groups/groups/groups.component.ts
Normal file
@ -0,0 +1,119 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { ActivatedRoute, RouterModule } from '@angular/router';
|
||||
import { Group } from '../../shared/models/group';
|
||||
import GroupService from '../../shared/services/group.service';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { GroupListComponent } from "../group-list/group-list.component";
|
||||
import { Policy } from '../../shared/models/policy';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { GroupItemEditComponent } from '../group-item-edit/group-item-edit.component';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { HermesClientService } from '../../hermes-client.service';
|
||||
import { GroupChatter } from '../../shared/models/group-chatter';
|
||||
|
||||
@Component({
|
||||
selector: 'groups',
|
||||
imports: [
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
MatMenuModule,
|
||||
MatTableModule,
|
||||
RouterModule,
|
||||
GroupListComponent,
|
||||
],
|
||||
templateUrl: './groups.component.html',
|
||||
styleUrl: './groups.component.scss'
|
||||
})
|
||||
export class GroupsComponent {
|
||||
private readonly _groupService = inject(GroupService);
|
||||
private readonly _client = inject(HermesClientService);
|
||||
private readonly _route = inject(ActivatedRoute);
|
||||
private readonly _dialog = inject(MatDialog);
|
||||
|
||||
items: { group: Group, chatters: GroupChatter[], policies: Policy[] }[] = [];
|
||||
|
||||
constructor() {
|
||||
this._route.data.subscribe(payload => {
|
||||
const groups = payload['groups'];
|
||||
const chatters = payload['chatters'];
|
||||
const policies = payload['policies'];
|
||||
const elements: { group: Group, chatters: GroupChatter[], policies: Policy[] }[] = [];
|
||||
|
||||
for (let group of groups) {
|
||||
elements.push({
|
||||
group: group,
|
||||
chatters: chatters.filter((c: GroupChatter) => c.group_id == group.id),
|
||||
policies: policies.filter((p: Policy) => p.group_id == group.id),
|
||||
});
|
||||
}
|
||||
|
||||
this.items = elements;
|
||||
});
|
||||
|
||||
this._groupService.createGroup$?.subscribe(d => {
|
||||
if (d.error || !d.data || d.request.nounce != null && d.request.nounce.startsWith(this._client.session_id))
|
||||
return;
|
||||
|
||||
let index = -1;
|
||||
for (let i = 0; i < this.items.length; i++) {
|
||||
const comp = this.compare(d.data, this.items[i].group);
|
||||
if (comp < 0) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.items.splice(index >= 0 ? index : this.items.length, 0, { group: d.data, chatters: [], policies: [] });
|
||||
});
|
||||
|
||||
this._groupService.updateGroup$?.subscribe(d => {
|
||||
if (d.error || !d.data || d.request.nounce != null && d.request.nounce.startsWith(this._client.session_id))
|
||||
return;
|
||||
|
||||
const group = this.items.find(r => r.group.id = d.data.id)?.group;
|
||||
if (group) {
|
||||
group.id = d.data.id;
|
||||
group.name = d.data.name;
|
||||
group.priority = d.data.priority;
|
||||
}
|
||||
});
|
||||
|
||||
this._groupService.deleteGroup$?.subscribe(d => {
|
||||
if (d.error || d.request.nounce != null && d.request.nounce.startsWith(this._client.session_id))
|
||||
return;
|
||||
|
||||
this.items = this.items.filter(r => r.group.id != d.request.data.id);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
openDialog(groupName: string): void {
|
||||
const group = { id: '', user_id: '', name: groupName, priority: 0 };
|
||||
const dialogRef = this._dialog.open(GroupItemEditComponent, {
|
||||
data: { group, isSpecial: groupName.length > 0 },
|
||||
});
|
||||
|
||||
const isNewGroup = group.id.length <= 0;
|
||||
dialogRef.afterClosed().subscribe((result: Group | undefined) => {
|
||||
if (!result)
|
||||
return;
|
||||
|
||||
|
||||
if (isNewGroup) {
|
||||
this.items.push({ group: result, chatters: [], policies: [] });
|
||||
} else {
|
||||
const same = this.items.find(i => i.group.id == group.id);
|
||||
if (same == null)
|
||||
return;
|
||||
|
||||
same.group.name = result.name;
|
||||
same.group.priority = result.priority;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
compare(a: Group, b: Group) {
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
}
|
@ -22,10 +22,6 @@ export class HermesClientService {
|
||||
constructor(private socket: HermesSocketService, private events: EventService) {
|
||||
this.connected = false;
|
||||
this.logged_in = false;
|
||||
|
||||
this.events.listen('tts_login', (payload) => {
|
||||
this.login(payload);
|
||||
});
|
||||
}
|
||||
|
||||
public connect() {
|
||||
@ -48,14 +44,14 @@ export class HermesClientService {
|
||||
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(
|
||||
filter(predicate),
|
||||
map(d => d.d)
|
||||
);
|
||||
}
|
||||
|
||||
public filterByRequestType(requestName: string): Observable<any>|undefined {
|
||||
public filterByRequestType(requestName: string): Observable<any> | undefined {
|
||||
return this.socket.get$()?.pipe(
|
||||
filter(d => d.op == 4 && d.d.request.type === requestName),
|
||||
map(d => d.d)
|
||||
@ -86,7 +82,18 @@ export class HermesClientService {
|
||||
api_key,
|
||||
web_login: true,
|
||||
major_version: 0,
|
||||
minor_version: 1
|
||||
minor_version: 4
|
||||
});
|
||||
}
|
||||
|
||||
public createGroup(name: string, priority: number) {
|
||||
if (!this.logged_in)
|
||||
return;
|
||||
|
||||
this.send(3, {
|
||||
request_id: null,
|
||||
type: "create_group",
|
||||
data: { name, priority },
|
||||
});
|
||||
}
|
||||
|
||||
@ -139,6 +146,17 @@ export class HermesClientService {
|
||||
});
|
||||
}
|
||||
|
||||
public deleteGroup(id: string) {
|
||||
if (!this.logged_in)
|
||||
return;
|
||||
|
||||
this.send(3, {
|
||||
request_id: null,
|
||||
type: "delete_group",
|
||||
data: { id },
|
||||
});
|
||||
}
|
||||
|
||||
public deletePolicy(id: string) {
|
||||
if (!this.logged_in)
|
||||
return;
|
||||
@ -197,6 +215,17 @@ export class HermesClientService {
|
||||
});
|
||||
}
|
||||
|
||||
public fetchGroups() {
|
||||
if (!this.logged_in)
|
||||
return;
|
||||
|
||||
this.send(3, {
|
||||
request_id: null,
|
||||
type: "get_groups",
|
||||
data: null,
|
||||
});
|
||||
}
|
||||
|
||||
public fetchPermissionsAndGroups() {
|
||||
if (!this.logged_in)
|
||||
return;
|
||||
@ -265,6 +294,17 @@ export class HermesClientService {
|
||||
});
|
||||
}
|
||||
|
||||
public updateGroup(id: string, name: string, priority: number) {
|
||||
if (!this.logged_in)
|
||||
return;
|
||||
|
||||
this.send(3, {
|
||||
request_id: null,
|
||||
type: "update_group",
|
||||
data: { id, name, priority },
|
||||
});
|
||||
}
|
||||
|
||||
public updatePolicy(id: string, groupId: string, path: string, usage: number, timespan: number) {
|
||||
if (!this.logged_in)
|
||||
return;
|
||||
@ -320,13 +360,13 @@ export class HermesClientService {
|
||||
console.log("RX:", message);
|
||||
switch (message.op) {
|
||||
case 0: // Heartbeat
|
||||
console.log("Heartbeat received. Potential connection problem?");
|
||||
console.log("TTS Heartbeat received. Potential connection problem?");
|
||||
break;
|
||||
case 2: // Login Ack
|
||||
console.log("Login successful.");
|
||||
console.log("TTS Login successful.");
|
||||
this.logged_in = true;
|
||||
this.session_id = message.d.session_id;
|
||||
this.events.emit('tts_login_ack', null);
|
||||
this.events.emit('tts_login_ack', message.d);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { OnInit, Injectable } from '@angular/core';
|
||||
import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
|
||||
import { catchError, filter, first, timeout } from 'rxjs/operators';
|
||||
import { catchError, first, timeout } from 'rxjs/operators';
|
||||
import { environment } from '../environments/environment';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { EMPTY, Observable, Observer, throwError } from 'rxjs';
|
||||
|
||||
|
||||
@Injectable({
|
||||
@ -42,15 +42,16 @@ export class HermesSocketService implements OnInit {
|
||||
this.socket.next(msg);
|
||||
}
|
||||
|
||||
public get$(): Observable<any>|undefined {
|
||||
return this.socket?.asObservable();
|
||||
public get$(): Observable<any> | undefined {
|
||||
return this.socket?.asObservable().pipe(catchError(_ => EMPTY));
|
||||
}
|
||||
|
||||
public subscribe(subscriptions: any) {
|
||||
public subscribe(subscriptions: Partial<Observer<any>> | ((value: any) => void)) {
|
||||
if (!this.socket || this.socket.closed)
|
||||
return;
|
||||
|
||||
return this.socket.subscribe(subscriptions);
|
||||
return this.socket.pipe(catchError(_ => EMPTY))
|
||||
.subscribe(subscriptions)
|
||||
}
|
||||
|
||||
public close() {
|
||||
|
@ -1,35 +1,55 @@
|
||||
<nav>
|
||||
<user-card class="card" />
|
||||
<ul>
|
||||
@if (!isLoggedIn()) {
|
||||
<li>
|
||||
<a routerLink="/login" routerLinkActive="active" *ngIf="!isLoggedIn()">
|
||||
<a routerLink="/login"
|
||||
routerLinkActive="active">
|
||||
Login
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
@if (isLoggedIn() && !isTTSLoggedIn()) {
|
||||
<li>
|
||||
<a routerLink="/tts-login" routerLinkActive="active" *ngIf="isLoggedIn() && !isTTSLoggedIn()">
|
||||
<a routerLink="/tts-login"
|
||||
routerLinkActive="active">
|
||||
TTS Login
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
@if (isLoggedIn() && isTTSLoggedIn()) {
|
||||
<li>
|
||||
<a routerLink="/policies" routerLinkActive="active" *ngIf="isLoggedIn() && isTTSLoggedIn()">
|
||||
<a routerLink="/policies"
|
||||
routerLinkActive="active">
|
||||
Policies
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a routerLink="/filters" routerLinkActive="active" *ngIf="isLoggedIn() && isTTSLoggedIn()">
|
||||
<a routerLink="/filters"
|
||||
routerLinkActive="active">
|
||||
Filters
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a routerLink="/actions" routerLinkActive="active" *ngIf="isLoggedIn() && isTTSLoggedIn()">
|
||||
<a routerLink="/actions"
|
||||
routerLinkActive="active">
|
||||
Actions
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a routerLink="/redemptions" routerLinkActive="active" *ngIf="isLoggedIn() && isTTSLoggedIn()">
|
||||
<a routerLink="/redemptions"
|
||||
routerLinkActive="active">
|
||||
Redemptions
|
||||
</a>
|
||||
</li>
|
||||
@if (isAdmin()) {
|
||||
<li>
|
||||
<a routerLink="/groups"
|
||||
routerLinkActive="active">
|
||||
Groups
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
</ul>
|
||||
</nav>
|
@ -5,32 +5,33 @@ $secondary_background_color: #DDDDDD;
|
||||
$secondary_font_color: #333333;
|
||||
|
||||
ul {
|
||||
padding: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
list-style: none;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
list-style: none;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
a {
|
||||
background-color: transparent;
|
||||
padding: 1em;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
font-size: large;
|
||||
text-decoration: none;
|
||||
color: $primary_font_color;
|
||||
background-color: transparent;
|
||||
padding: 1em;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
font-size: large;
|
||||
text-decoration: none;
|
||||
color: $primary_font_color;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
background-color: #FAFAFA;
|
||||
background-color: #FAFAFA;
|
||||
}
|
||||
|
||||
a.active {
|
||||
background-color: #F5F5F5;
|
||||
background-color: #F5F5F5;
|
||||
}
|
@ -10,7 +10,7 @@ describe('NavigationComponent', () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [NavigationComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(NavigationComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { HermesClientService } from '../hermes-client.service';
|
||||
import { ApiAuthenticationService } from '../shared/services/api/api-authentication.service';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
@ -10,7 +9,7 @@ import { UserCardComponent } from "../auth/user-card/user-card.component";
|
||||
@Component({
|
||||
selector: 'navigation',
|
||||
standalone: true,
|
||||
imports: [CommonModule, RouterModule, MatCardModule, AuthModule, UserCardComponent],
|
||||
imports: [RouterModule, MatCardModule, AuthModule, UserCardComponent],
|
||||
templateUrl: './navigation.component.html',
|
||||
styleUrl: './navigation.component.scss'
|
||||
})
|
||||
|
@ -1,13 +1,19 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { PolicyComponent } from './policy/policy.component';
|
||||
import { PolicyTableComponent } from './policy-table/policy-table.component';
|
||||
import { PolicyAddFormComponent } from './policy-add-form/policy-add-form.component';
|
||||
import { PolicyItemEditComponent } from './policy-item-edit/policy-item-edit.component';
|
||||
import { PolicyAddButtonComponent } from './policy-add-button/policy-add-button.component';
|
||||
import { PolicyDropdownComponent } from './policy-dropdown/policy-dropdown.component';
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
imports: [
|
||||
PolicyComponent, PolicyTableComponent, PolicyAddFormComponent
|
||||
PolicyComponent,
|
||||
PolicyTableComponent,
|
||||
PolicyAddButtonComponent,
|
||||
PolicyItemEditComponent,
|
||||
PolicyDropdownComponent,
|
||||
]
|
||||
})
|
||||
export class PoliciesModule { }
|
@ -0,0 +1,5 @@
|
||||
<button mat-button
|
||||
(click)="openDialog()">
|
||||
<mat-icon>add</mat-icon>
|
||||
Add a policy
|
||||
</button>
|
@ -0,0 +1,3 @@
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { PolicyAddButtonComponent } from './policy-add-button.component';
|
||||
|
||||
describe('PolicyAddButtonComponent', () => {
|
||||
let component: PolicyAddButtonComponent;
|
||||
let fixture: ComponentFixture<PolicyAddButtonComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [PolicyAddButtonComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(PolicyAddButtonComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,44 @@
|
||||
import { Component, EventEmitter, inject, Input, Output } from '@angular/core';
|
||||
import { Policy } from '../../shared/models/policy';
|
||||
import { PolicyItemEditComponent } from '../policy-item-edit/policy-item-edit.component';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { Group } from '../../shared/models/group';
|
||||
|
||||
@Component({
|
||||
selector: 'policy-add-button',
|
||||
imports: [
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
],
|
||||
templateUrl: './policy-add-button.component.html',
|
||||
styleUrl: './policy-add-button.component.scss'
|
||||
})
|
||||
export class PolicyAddButtonComponent {
|
||||
private readonly dialog = inject(MatDialog);
|
||||
@Input({ required: true }) policies: Policy[] = [];
|
||||
@Input({ required: true }) groups: Group[] = [];
|
||||
@Input() group: string | undefined = undefined;
|
||||
@Output() policy = new EventEmitter<Policy>();
|
||||
|
||||
|
||||
openDialog(): void {
|
||||
const dialogRef = this.dialog.open(PolicyItemEditComponent, {
|
||||
data: {
|
||||
policies: this.policies,
|
||||
groups: this.groups,
|
||||
group_id: this.group,
|
||||
groupDisabled: !!this.group,
|
||||
isNew: true,
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result: Policy) => {
|
||||
if (!result)
|
||||
return;
|
||||
|
||||
this.policy.emit(result);
|
||||
});
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
<div>
|
||||
<form
|
||||
standalone>
|
||||
<mat-form-field>
|
||||
<input
|
||||
name="path"
|
||||
type="text"
|
||||
placeholder="Pick one"
|
||||
[(ngModel)]="newPolicyName"
|
||||
matInput
|
||||
[formControl]="myControl"
|
||||
[matAutocomplete]="auto" />
|
||||
<mat-autocomplete #auto="matAutocomplete">
|
||||
@for (option of filteredPolicies | async; track option) {
|
||||
<mat-option [value]="option">{{option}}</mat-option>
|
||||
}
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
<button
|
||||
mat-flat-button
|
||||
(click)="addNewPolicy()">
|
||||
Add
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
@ -1,74 +0,0 @@
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import { Component } from '@angular/core';
|
||||
import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import EventService from '../../shared/services/EventService';
|
||||
import { map, Observable, startWith } from 'rxjs';
|
||||
import { HermesClientService } from '../../hermes-client.service';
|
||||
|
||||
const Policies = [
|
||||
{ path: "tts", description: "Anything to do with TTS" },
|
||||
{ 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.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.subscriptions.read", description: "To read chat messages from subscriptions via TTS" },
|
||||
{ path: "tts.commands", description: "To execute commands for TTS" },
|
||||
{ path: "tts.commands.nightbot", description: "To use !nightbot command" },
|
||||
{ path: "tts.commands.obs", description: "To use !obs command" },
|
||||
{ path: "tts.commands.refresh", description: "To use !refresh command" },
|
||||
{ path: "tts.commands.skip", description: "To use !skip command" },
|
||||
{ path: "tts.commands.skipall", description: "To use !skipall 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.leave", description: "To use !tts leave command" },
|
||||
{ path: "tts.commands.version", description: "To use !version command" },
|
||||
{ path: "tts.commands.voice", description: "To use !voice command" },
|
||||
{ path: "tts.commands.voice.admin", description: "To use !voice command on others" },
|
||||
]
|
||||
|
||||
@Component({
|
||||
selector: 'policy-add-form',
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
FormsModule,
|
||||
MatAutocompleteModule,
|
||||
MatButtonModule,
|
||||
MatInputModule,
|
||||
ReactiveFormsModule,
|
||||
],
|
||||
templateUrl: './policy-add-form.component.html',
|
||||
styleUrl: './policy-add-form.component.scss'
|
||||
})
|
||||
export class PolicyAddFormComponent {
|
||||
myControl = new FormControl('');
|
||||
newPolicyName: string = '';
|
||||
filteredPolicies: Observable<string[]>;
|
||||
|
||||
constructor(private events: EventService, private hermes: HermesClientService) {
|
||||
this.filteredPolicies = this.myControl.valueChanges.pipe(
|
||||
startWith(''),
|
||||
map(value => this._filter(value || '')),
|
||||
);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
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 = "";
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
<mat-form-field>
|
||||
<mat-label>Path</mat-label>
|
||||
<input name="path"
|
||||
matInput
|
||||
type="text"
|
||||
placeholder="Pick a policy..."
|
||||
[formControl]="policyControl"
|
||||
[matAutocomplete]="auto" />
|
||||
<mat-autocomplete #auto="matAutocomplete">
|
||||
@for (option of filteredPolicies | async; track option) {
|
||||
<mat-option [value]="option">{{option}}</mat-option>
|
||||
}
|
||||
</mat-autocomplete>
|
||||
@if (policyControl.invalid && (policyControl.dirty || policyControl.touched)) {
|
||||
@if (policyControl.hasError('required')) {
|
||||
<small class="error">This field is required.</small>
|
||||
}
|
||||
}
|
||||
</mat-form-field>
|
@ -0,0 +1,3 @@
|
||||
.error {
|
||||
color: #ba1a1a;
|
||||
}
|
@ -1,18 +1,18 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { PolicyAddFormComponent } from './policy-add-form.component';
|
||||
import { PolicyDropdownComponent } from './policy-dropdown.component';
|
||||
|
||||
describe('PolicyAddFormComponent', () => {
|
||||
let component: PolicyAddFormComponent;
|
||||
let fixture: ComponentFixture<PolicyAddFormComponent>;
|
||||
let component: PolicyDropdownComponent;
|
||||
let fixture: ComponentFixture<PolicyDropdownComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [PolicyAddFormComponent]
|
||||
imports: [PolicyDropdownComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(PolicyAddFormComponent);
|
||||
fixture = TestBed.createComponent(PolicyDropdownComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
@ -0,0 +1,72 @@
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { FormControl, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'
|
||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { map, Observable, startWith } from 'rxjs';
|
||||
|
||||
const Policies = [
|
||||
{ path: "tts", description: "Anything to do with TTS" },
|
||||
{ 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.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.subscriptions.read", description: "To read chat messages from subscriptions via TTS" },
|
||||
{ path: "tts.commands", description: "To execute commands for TTS" },
|
||||
{ path: "tts.commands.nightbot", description: "To use !nightbot command" },
|
||||
{ path: "tts.commands.obs", description: "To use !obs command" },
|
||||
{ path: "tts.commands.refresh", description: "To use !refresh command" },
|
||||
{ path: "tts.commands.skip", description: "To use !skip command" },
|
||||
{ path: "tts.commands.skipall", description: "To use !skipall 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.leave", description: "To use !tts leave command" },
|
||||
{ path: "tts.commands.version", description: "To use !version command" },
|
||||
{ path: "tts.commands.voice", description: "To use !voice command" },
|
||||
{ path: "tts.commands.voice.admin", description: "To use !voice command on others" },
|
||||
]
|
||||
|
||||
@Component({
|
||||
selector: 'policy-dropdown',
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
FormsModule,
|
||||
MatAutocompleteModule,
|
||||
MatButtonModule,
|
||||
MatInputModule,
|
||||
ReactiveFormsModule,
|
||||
],
|
||||
templateUrl: './policy-dropdown.component.html',
|
||||
styleUrl: './policy-dropdown.component.scss'
|
||||
})
|
||||
export class PolicyDropdownComponent {
|
||||
@Input() policy: string | null = '';
|
||||
policyControl = new FormControl('', [Validators.required]);
|
||||
filteredPolicies: Observable<string[]>;
|
||||
|
||||
constructor() {
|
||||
this.filteredPolicies = this.policyControl.valueChanges.pipe(
|
||||
startWith(''),
|
||||
map(value => this._filter(value || '')),
|
||||
);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.policyControl.setValue(this.policy);
|
||||
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;
|
||||
}
|
||||
|
||||
return names.filter(option => option.toLowerCase().includes(filterValue));
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title>{{isNew ? 'Add' : 'Edit'}} Policy</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<group-dropdown ngDefaultControl
|
||||
[formControl]="groupControl"
|
||||
[groups]="data.groups"
|
||||
[group]="data.group_id"
|
||||
[groupDisabled]="data.groupDisabled"
|
||||
[errorMessages]="groupErrorMessages" />
|
||||
<policy-dropdown #policyDropdown [policy]="pathControl.value" />
|
||||
<mat-form-field>
|
||||
<mat-label>Usage</mat-label>
|
||||
<input matInput
|
||||
type="number"
|
||||
[formControl]="usageControl" />
|
||||
@if (usageControl.invalid && (usageControl.dirty || usageControl.touched)) {
|
||||
@if (usageControl.hasError('required')) {
|
||||
<small class="error">This field is required.</small>
|
||||
}
|
||||
@if (usageControl.hasError('min')) {
|
||||
<small class="error">The value needs to be positive.</small>
|
||||
}
|
||||
@if (usageControl.hasError('max')) {
|
||||
<small class="error">The value needs to be lower than 100.</small>
|
||||
}
|
||||
@if (usageControl.hasError('integer')) {
|
||||
<small class="error">The value needs to be an integer.</small>
|
||||
}
|
||||
}
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-label>Span</mat-label>
|
||||
<input matInput
|
||||
type="number"
|
||||
[formControl]="spanControl" />
|
||||
@if (spanControl.invalid && (spanControl.dirty || spanControl.touched)) {
|
||||
@if (spanControl.hasError('required')) {
|
||||
<small class="error">This field is required.</small>
|
||||
}
|
||||
@if (spanControl.hasError('min')) {
|
||||
<small class="error">The value needs to be at least 1000.</small>
|
||||
}
|
||||
@if (spanControl.hasError('max')) {
|
||||
<small class="error">The value needs to be lower than 86401.</small>
|
||||
}
|
||||
@if (spanControl.hasError('integer')) {
|
||||
<small class="error">The value needs to be an integer.</small>
|
||||
}
|
||||
}
|
||||
</mat-form-field>
|
||||
</mat-card-content>
|
||||
<mat-card-actions>
|
||||
@if (isNew) {
|
||||
<button mat-button
|
||||
(click)="save()">
|
||||
<mat-icon>add</mat-icon>Add
|
||||
</button>
|
||||
} @else {
|
||||
<button mat-button
|
||||
(click)="save()">
|
||||
<mat-icon>save</mat-icon>Save
|
||||
</button>
|
||||
}
|
||||
<button mat-button
|
||||
(click)="dialogRef.close()">
|
||||
<mat-icon>cancel</mat-icon>Cancel
|
||||
</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
@ -0,0 +1,10 @@
|
||||
.mat-mdc-card-content {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-auto-flow: row dense;
|
||||
grid-gap: 0 1em;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #ba1a1a;
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { PolicyItemEditComponent } from './policy-item-edit.component';
|
||||
|
||||
describe('PolicyItemEditComponent', () => {
|
||||
let component: PolicyItemEditComponent;
|
||||
let fixture: ComponentFixture<PolicyItemEditComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [PolicyItemEditComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(PolicyItemEditComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
107
src/app/policies/policy-item-edit/policy-item-edit.component.ts
Normal file
107
src/app/policies/policy-item-edit/policy-item-edit.component.ts
Normal file
@ -0,0 +1,107 @@
|
||||
import { AfterViewInit, Component, inject, OnInit, ViewChild } from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { HermesClientService } from '../../hermes-client.service';
|
||||
import { GroupDropdownComponent } from '../../groups/group-dropdown/group-dropdown.component';
|
||||
import { Group } from '../../shared/models/group';
|
||||
import { Policy } from '../../shared/models/policy';
|
||||
import { PolicyDropdownComponent } from '../policy-dropdown/policy-dropdown.component';
|
||||
|
||||
@Component({
|
||||
selector: 'policy-item-edit',
|
||||
imports: [
|
||||
GroupDropdownComponent,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
MatFormFieldModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
ReactiveFormsModule,
|
||||
PolicyDropdownComponent
|
||||
],
|
||||
templateUrl: './policy-item-edit.component.html',
|
||||
styleUrl: './policy-item-edit.component.scss'
|
||||
})
|
||||
export class PolicyItemEditComponent implements OnInit, AfterViewInit {
|
||||
private readonly client = inject(HermesClientService);
|
||||
readonly data = inject(MAT_DIALOG_DATA);
|
||||
readonly dialogRef = inject(MatDialogRef<PolicyItemEditComponent>);
|
||||
|
||||
readonly groupControl = new FormControl<Group | string | undefined>(undefined, [Validators.required]);
|
||||
readonly usageControl = new FormControl(1, [Validators.required, Validators.min(1), Validators.max(99)]);
|
||||
readonly spanControl = new FormControl(5000, [Validators.required, Validators.min(1000), Validators.max(86400)]);
|
||||
pathControl = new FormControl('', [Validators.required]);
|
||||
|
||||
readonly groupErrorMessages = {
|
||||
'required': 'This field is required.'
|
||||
};
|
||||
|
||||
readonly formGroup = new FormGroup({
|
||||
group: this.groupControl,
|
||||
path: this.pathControl,
|
||||
usage: this.usageControl,
|
||||
span: this.spanControl,
|
||||
});
|
||||
|
||||
isNew: boolean = false;
|
||||
waitForResponse: boolean = false;
|
||||
|
||||
@ViewChild('policyDropdown') policyDropdown: PolicyDropdownComponent | undefined;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.isNew = this.data.isNew;
|
||||
|
||||
if (!this.data.policy_id)
|
||||
return;
|
||||
|
||||
const policy = this.data.policies.find((p: Policy) => p.id == this.data.policy_id);
|
||||
if (!policy)
|
||||
return;
|
||||
|
||||
this.groupControl.setValue(policy.group_id);
|
||||
this.pathControl.setValue(policy.path);
|
||||
this.usageControl.setValue(policy.usage);
|
||||
this.spanControl.setValue(policy.span);
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
if (this.policyDropdown) {
|
||||
this.pathControl = this.policyDropdown.policyControl;
|
||||
this.formGroup.setControl('path', this.pathControl);
|
||||
}
|
||||
}
|
||||
|
||||
save() {
|
||||
if (this.formGroup.invalid || this.waitForResponse)
|
||||
return;
|
||||
|
||||
this.waitForResponse = true;
|
||||
const group_id = (this.groupControl.value as Group)!.id;
|
||||
const path = this.pathControl.value!;
|
||||
const usage = this.usageControl.value!;
|
||||
const span = this.spanControl.value!;
|
||||
|
||||
if (this.isNew) {
|
||||
this.client.first((d: any) => d.op == 4 && d.d.request.type == 'create_policy' && d.d.data.group_id == group_id && d.d.data.path == path && d.d.data.usage == usage && d.d.data.span == span)
|
||||
.subscribe({
|
||||
next: (d) => this.dialogRef.close(d.d.data),
|
||||
error: () => this.waitForResponse = false,
|
||||
complete: () => this.waitForResponse = false,
|
||||
});
|
||||
this.client.createPolicy(group_id, path, usage, span);
|
||||
} else {
|
||||
this.client.first((d: any) => d.op == 4 && d.d.request.type == 'update_policy' && d.d.data.id == this.data.policy_id && d.d.data.group_id == group_id && d.d.data.path == path && d.d.data.usage == usage && d.d.data.span == span)
|
||||
.subscribe({
|
||||
next: (d) => this.dialogRef.close(d.d.data),
|
||||
error: () => this.waitForResponse = false,
|
||||
complete: () => this.waitForResponse = false,
|
||||
});
|
||||
this.client.updatePolicy(this.data.policy_id, group_id, path, usage, span);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,61 +1,59 @@
|
||||
<table mat-table [dataSource]="policies" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="path">
|
||||
<th mat-header-cell *matHeaderCellDef>Path</th>
|
||||
<td mat-cell *matCellDef="let policy">
|
||||
{{policy.path}}
|
||||
</td>
|
||||
</ng-container>
|
||||
<table mat-table
|
||||
[dataSource]="policies"
|
||||
class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="path">
|
||||
<th mat-header-cell
|
||||
*matHeaderCellDef>Path</th>
|
||||
<td mat-cell
|
||||
*matCellDef="let policy">
|
||||
{{policy.path}}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="group">
|
||||
<th mat-header-cell *matHeaderCellDef>Group</th>
|
||||
<td mat-cell *matCellDef="let policy">
|
||||
@if (policy.editing) {
|
||||
<input type="text" [(ngModel)]="policy.temp_group_name" />
|
||||
}
|
||||
@if (!policy.editing && groups[policy.group_id]) {
|
||||
{{groups[policy.group_id].name}}
|
||||
}
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="group">
|
||||
<th mat-header-cell
|
||||
*matHeaderCellDef>Group</th>
|
||||
<td mat-cell
|
||||
*matCellDef="let policy">
|
||||
{{getGroupById(policy.group_id)?.name || '\<unknown group\>'}}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="usage">
|
||||
<th mat-header-cell *matHeaderCellDef>Usage Rate</th>
|
||||
<td mat-cell *matCellDef="let policy">
|
||||
@if (policy.editing) {
|
||||
<input type="number" [(ngModel)]="policy.usage" (keypress)="($event.charCode >= 48 && $event.charCode < 58)" />
|
||||
}
|
||||
@if (!policy.editing) {
|
||||
{{policy.usage}}
|
||||
}
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="usage">
|
||||
<th mat-header-cell
|
||||
*matHeaderCellDef>Usage Rate</th>
|
||||
<td mat-cell
|
||||
class="center"
|
||||
*matCellDef="let policy">
|
||||
{{policy.usage}}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="span">
|
||||
<th mat-header-cell *matHeaderCellDef>Span (ms)</th>
|
||||
<td mat-cell *matCellDef="let policy">
|
||||
@if (policy.editing) {
|
||||
<input type="number" [(ngModel)]="policy.span" (keypress)="($event.charCode >= 48 && $event.charCode < 58)" />
|
||||
}
|
||||
@if (!policy.editing) {
|
||||
{{policy.span}}
|
||||
}
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="span">
|
||||
<th mat-header-cell
|
||||
*matHeaderCellDef>Span (ms)</th>
|
||||
<td mat-cell
|
||||
class="center"
|
||||
*matCellDef="let policy">
|
||||
{{policy.span}}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef> Actions </th>
|
||||
<td mat-cell *matCellDef="let policy">
|
||||
@if (!policy.editing) {
|
||||
<button mat-mini-fab (click)="edit(policy)"><mat-icon>edit</mat-icon></button>
|
||||
<button mat-mini-fab (click)="delete(policy)"><mat-icon>delete</mat-icon></button>
|
||||
}
|
||||
@if (policy.editing) {
|
||||
<button mat-mini-fab (click)="save(policy)"><mat-icon>save</mat-icon></button>
|
||||
<button mat-mini-fab (click)="cancel(policy)"><mat-icon>cancel</mat-icon></button>
|
||||
}
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell
|
||||
*matHeaderCellDef> Actions </th>
|
||||
<td mat-cell
|
||||
*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>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
<tr mat-header-row
|
||||
*matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row
|
||||
*matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
@ -0,0 +1,12 @@
|
||||
table {
|
||||
border-radius: 15px;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.delete {
|
||||
color: red;
|
||||
}
|
||||
|
||||
button~button {
|
||||
margin-left: 1em;
|
||||
}
|
@ -10,7 +10,7 @@ describe('PolicyTableComponent', () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [PolicyTableComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(PolicyTableComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
@ -1,119 +1,83 @@
|
||||
import { Component, ElementRef, Input, isDevMode, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||
import { AfterViewInit, Component, inject, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, ViewChild } from '@angular/core';
|
||||
import { MatTable, MatTableModule } from '@angular/material/table';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import EventService from '../../shared/services/EventService';
|
||||
import { Policy } from '../../shared/models/policy';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { HermesClientService } from '../../hermes-client.service';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { PolicyItemEditComponent } from '../policy-item-edit/policy-item-edit.component';
|
||||
import { Group } from '../../shared/models/group';
|
||||
|
||||
@Component({
|
||||
selector: 'policy-table',
|
||||
imports: [FormsModule, MatTableModule, MatIconModule],
|
||||
imports: [FormsModule, MatButtonModule, MatTableModule, MatIconModule],
|
||||
templateUrl: './policy-table.component.html',
|
||||
styleUrl: './policy-table.component.scss'
|
||||
})
|
||||
export class PolicyTableComponent implements OnInit, OnDestroy {
|
||||
@Input() policies: Policy[] = []
|
||||
displayedColumns = ['path', 'group', 'usage', 'span', 'actions']
|
||||
groups: { [id: string]: { id: string, name: string, priority: number } }
|
||||
private readonly _subscriptions: Subscription[] = [];
|
||||
export class PolicyTableComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly hermes = inject(HermesClientService);
|
||||
private readonly events = inject(EventService);
|
||||
private readonly dialog = inject(MatDialog);
|
||||
|
||||
@Input() policies: Policy[] = [];
|
||||
@ViewChild(MatTable) table: MatTable<Policy>;
|
||||
private subscription: Subscription | undefined;
|
||||
|
||||
constructor(private events: EventService, private hermes: HermesClientService) {
|
||||
readonly displayedColumns = ['path', 'group', 'usage', 'span', 'actions'];
|
||||
private readonly _subscriptions: any[] = [];
|
||||
|
||||
groups: Group[] = [];
|
||||
|
||||
|
||||
constructor() {
|
||||
this.table = {} as MatTable<Policy>;
|
||||
this.groups = {};
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this._subscriptions.push(this.events.listen('addPolicy', (payload) => {
|
||||
if (!payload)
|
||||
return;
|
||||
if (this.policies.map(p => p.path).includes(payload)) {
|
||||
return;
|
||||
}
|
||||
this.route.data.subscribe(r => {
|
||||
this.groups = [...r['groups']];
|
||||
});
|
||||
|
||||
this.policies.push(new Policy("", "", payload, 1, 5000, "", true, true));
|
||||
this._subscriptions.push(this.events.listen('addPolicy', (payload) => {
|
||||
if (!payload || this.policies.map(p => p.path).includes(payload))
|
||||
return;
|
||||
|
||||
this.policies.push(payload);
|
||||
this.table.renderRows();
|
||||
}));
|
||||
|
||||
const subscription = this.hermes.subscribe(4, (response: any) => {
|
||||
if (response.request.type == "get_policies") {
|
||||
for (let policy of response.data) {
|
||||
this.policies.push(new Policy(policy.id, policy.group_id, policy.path, policy.usage, policy.span, "", false, false));
|
||||
}
|
||||
this.table.renderRows();
|
||||
} else if (response.request.type == "create_policy") {
|
||||
const policy = this.policies.find(p => this.groups[response.data.group_id].name == p.temp_group_name && p.path == response.data.path);
|
||||
if (policy == null) {
|
||||
this.policies.push(new Policy(response.data.id, response.data.group_id, response.data.path, response.data.usage, response.data.span));
|
||||
} else {
|
||||
policy.id = response.data.id;
|
||||
policy.group_id = response.data.group_id;
|
||||
policy.editing = false;
|
||||
policy.isNew = false;
|
||||
}
|
||||
this.table.renderRows();
|
||||
} else if (response.request.type == "update_policy") {
|
||||
const policy = this.policies.find(p => p.id == response.data.id);
|
||||
if (policy == null) {
|
||||
this.policies.push(new Policy(response.data.id, response.data.group_id, response.data.path, response.data.usage, response.data.span));
|
||||
} else {
|
||||
policy.id = response.data.id;
|
||||
policy.group_id = response.data.group_id;
|
||||
policy.editing = false;
|
||||
policy.isNew = false;
|
||||
}
|
||||
this.table.renderRows();
|
||||
} else if (response.request.type == "delete_policy") {
|
||||
const policy = this.policies.find(p => p.id == response.request.data.id);
|
||||
if (!policy) {
|
||||
return;
|
||||
}
|
||||
const index = this.policies.indexOf(policy);
|
||||
if (index >= 0) {
|
||||
this.policies.splice(index, 1);
|
||||
this.table.renderRows();
|
||||
}
|
||||
} else if (response.request.type == "get_permissions") {
|
||||
this.groups = Object.assign({}, ...response.data.groups.map((g: any) => ({ [g.id]: g })));
|
||||
this._subscriptions.push(this.hermes.subscribeToRequests('create_policy', response => {
|
||||
const policy = this.policies.find(p => p.path == response.data.path);
|
||||
if (policy == null) {
|
||||
this.policies.push(response.data);
|
||||
}
|
||||
});
|
||||
this.table.renderRows();
|
||||
}));
|
||||
|
||||
if (subscription) {
|
||||
this._subscriptions.push(subscription);
|
||||
}
|
||||
this._subscriptions.push(this.hermes.subscribeToRequests('update_policy', response => {
|
||||
const policy = this.policies.find(p => p.id == response.data.id);
|
||||
if (policy != null) {
|
||||
policy.id = response.data.id;
|
||||
policy.group_id = response.data.group_id;
|
||||
}
|
||||
this.table.renderRows();
|
||||
}));
|
||||
|
||||
this.hermes.fetchPolicies();
|
||||
this.hermes.fetchPermissionsAndGroups();
|
||||
this._subscriptions.push(this.hermes.subscribeToRequests('delete_policy', response => {
|
||||
this.policies = this.policies.filter(p => p.id != response.request.data.id);
|
||||
this.table.renderRows();
|
||||
}));
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.table.renderRows();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this._subscriptions.length > 0)
|
||||
this._subscriptions.forEach(s => s.unsubscribe());
|
||||
}
|
||||
|
||||
cancel(policy: Policy) {
|
||||
if (!policy.editing)
|
||||
return;
|
||||
|
||||
if (policy.isNew) {
|
||||
const index = this.policies.indexOf(policy);
|
||||
if (index >= 0) {
|
||||
this.policies.splice(index, 1);
|
||||
this.table.renderRows();
|
||||
}
|
||||
} else {
|
||||
policy.path = policy.old_path ?? '';
|
||||
policy.usage = policy.old_usage ?? 1;
|
||||
policy.span = policy.old_span ?? 5000;
|
||||
policy.old_path = undefined;
|
||||
policy.old_span = undefined;
|
||||
policy.old_usage = undefined;
|
||||
policy.editing = false;
|
||||
}
|
||||
this._subscriptions.filter(s => !!s).forEach(s => s.unsubscribe());
|
||||
}
|
||||
|
||||
delete(policy: Policy) {
|
||||
@ -121,62 +85,29 @@ export class PolicyTableComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
edit(policy: Policy) {
|
||||
policy.old_path = policy.path;
|
||||
policy.old_span = policy.span;
|
||||
policy.old_usage = policy.usage;
|
||||
policy.temp_group_name = this.groups[policy.group_id].name
|
||||
policy.editing = true;
|
||||
}
|
||||
|
||||
save(policy: Policy) {
|
||||
if (!policy.temp_group_name) {
|
||||
console.log('group must be valid.');
|
||||
return;
|
||||
}
|
||||
const group = Object.values(this.groups).find(g => g.name == policy.temp_group_name);
|
||||
if (group == null) {
|
||||
console.log('group does not exist.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (policy.isNew) {
|
||||
const match = this.policies.find(p => p.group_id == group.id && p.path == policy.path);
|
||||
if (match) {
|
||||
console.log('policy already exists');
|
||||
return;
|
||||
const dialogRef = this.dialog.open(PolicyItemEditComponent, {
|
||||
data: {
|
||||
policies: this.policies,
|
||||
groups: this.groups,
|
||||
policy_id: policy.id,
|
||||
group_id: policy.group_id,
|
||||
groupDisabled: true,
|
||||
isNew: false,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (isNaN(policy.usage)) {
|
||||
console.log('usage must be a whole number.');
|
||||
return;
|
||||
}
|
||||
if (policy.usage < 1 || policy.usage > 99) {
|
||||
console.error('usage must be between 1 and 99.');
|
||||
return;
|
||||
}
|
||||
if (policy.usage % 1.0 != 0) {
|
||||
console.error('usage must be a whole number.');
|
||||
return;
|
||||
}
|
||||
dialogRef.afterClosed().subscribe((result: Policy) => {
|
||||
if (!result)
|
||||
return;
|
||||
|
||||
if (isNaN(policy.span)) {
|
||||
console.log('span must be a whole number.');
|
||||
return;
|
||||
}
|
||||
if (policy.span < 1000 || policy.span > 1800000) {
|
||||
console.error('span must be between 1 and 1800000.');
|
||||
return;
|
||||
}
|
||||
if (policy.span % 1.0 != 0) {
|
||||
console.error('span must be a whole number.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (policy.isNew) {
|
||||
this.hermes.createPolicy(group.id, policy.path, policy.usage, policy.span);
|
||||
} else {
|
||||
this.hermes.updatePolicy(policy.id, group.id, policy.path, policy.usage, policy.span);
|
||||
}
|
||||
policy.group_id = result.group_id;
|
||||
policy.path = result.path;
|
||||
policy.usage = result.usage;
|
||||
policy.span = result.span;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getGroupById(group_id: string) {
|
||||
return this.groups.find((g: Group) => g.id == group_id);
|
||||
}
|
||||
}
|
@ -1,8 +1,10 @@
|
||||
<h4>Policies</h4>
|
||||
<div>
|
||||
<policy-add-form />
|
||||
<div class="add">
|
||||
<policy-add-button [policies]="policies"
|
||||
[groups]="groups"
|
||||
(policy)="addPolicy($event)" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<policy-table [policies]="policies" />
|
||||
<policy-table [policies]="policies" />
|
||||
</div>
|
@ -1,3 +1,9 @@
|
||||
h4 {
|
||||
text-align: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.add {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 2em;
|
||||
margin: 1em 2em 2em;
|
||||
}
|
@ -10,7 +10,7 @@ describe('PolicyComponent', () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [PolicyComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(PolicyComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
@ -1,42 +1,51 @@
|
||||
import { Component, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
|
||||
import { PolicyAddFormComponent } from "../policy-add-form/policy-add-form.component";
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { PolicyTableComponent } from "../policy-table/policy-table.component";
|
||||
import { Policy, PolicyScope } from '../../shared/models/policy';
|
||||
import { isPlatformBrowser } from '@angular/common';
|
||||
import { HermesClientService } from '../../hermes-client.service';
|
||||
import { Router, RouterModule } from '@angular/router';
|
||||
import { Policy } from '../../shared/models/policy';
|
||||
import { ActivatedRoute, RouterModule } from '@angular/router';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { PolicyAddButtonComponent } from "../policy-add-button/policy-add-button.component";
|
||||
import { Group } from '../../shared/models/group';
|
||||
|
||||
@Component({
|
||||
selector: 'policy',
|
||||
imports: [RouterModule, PolicyAddFormComponent, PolicyTableComponent],
|
||||
templateUrl: './policy.component.html',
|
||||
styleUrl: './policy.component.scss'
|
||||
selector: 'policy',
|
||||
imports: [MatButtonModule, MatIconModule, PolicyTableComponent, RouterModule, PolicyAddButtonComponent],
|
||||
templateUrl: './policy.component.html',
|
||||
styleUrl: './policy.component.scss'
|
||||
})
|
||||
export class PolicyComponent implements OnInit, OnDestroy {
|
||||
private isBrowser: boolean;
|
||||
items: Policy[];
|
||||
export class PolicyComponent {
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private _policies: Policy[] = [];
|
||||
groups: Group[] = [];
|
||||
|
||||
|
||||
constructor(private client: HermesClientService, private router: Router, @Inject(PLATFORM_ID) private platformId: Object) {
|
||||
this.isBrowser = isPlatformBrowser(this.platformId)
|
||||
constructor() {
|
||||
this.route.data.subscribe((data) => {
|
||||
const policies = [...data['policies']];
|
||||
policies.sort(this.compare);
|
||||
this._policies = policies;
|
||||
|
||||
this.items = []
|
||||
this.groups = [...data['groups']];
|
||||
});
|
||||
}
|
||||
|
||||
get policies() {
|
||||
return this._policies;
|
||||
}
|
||||
|
||||
addPolicy(policy: Policy) {
|
||||
let index = -1;
|
||||
for (let i = 0; i < this._policies.length; i++) {
|
||||
const comp = this.compare(policy, this._policies[i]);
|
||||
if (comp < 0) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
this._policies.splice(index >= 0 ? index : this._policies.length, 0, policy);
|
||||
}
|
||||
|
||||
get policies() {
|
||||
return this.items;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (!this.isBrowser)
|
||||
return;
|
||||
|
||||
if (!this.client.logged_in) {
|
||||
this.router.navigate(["tts-login"]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
}
|
||||
compare(a: Policy, b: Policy) {
|
||||
return a.path.localeCompare(b.path);
|
||||
}
|
||||
}
|
@ -5,23 +5,24 @@
|
||||
</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<twitch-redemption-dropdown
|
||||
ngDefaultControl
|
||||
[formControl]="redemptionFormControl"
|
||||
[errorMessages]="redemptionErrorMessages"
|
||||
[twitchRedemptions]="twitchRedemptions"
|
||||
[(twitchRedemptionId)]="redemption.redemption_id" />
|
||||
<twitch-redemption-dropdown ngDefaultControl
|
||||
[formControl]="redemptionFormControl"
|
||||
[errorMessages]="redemptionErrorMessages"
|
||||
[twitchRedemptions]="twitchRedemptions"
|
||||
[(twitchRedemptionId)]="redemption.redemption_id" />
|
||||
|
||||
<action-dropdown ngDefaultControl
|
||||
[formControl]="actionFormControl"
|
||||
[errorMessages]="actionErrorMessages"
|
||||
[actions]="redeemableActions"
|
||||
[(action)]="redemption.action_name" />
|
||||
|
||||
<action-dropdown
|
||||
ngDefaultControl
|
||||
[formControl]="actionFormControl"
|
||||
[errorMessages]="actionErrorMessages"
|
||||
[actions]="redeemableActions"
|
||||
[(action)]="redemption.action_name" />
|
||||
|
||||
<mat-form-field>
|
||||
<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)) {
|
||||
@for (error of orderErrorMessageKeys; track $index) {
|
||||
@if (orderFormControl.hasError(error)) {
|
||||
@ -31,19 +32,17 @@
|
||||
}
|
||||
</mat-form-field>
|
||||
<div class="buttons">
|
||||
<button
|
||||
mat-icon-button
|
||||
class="save"
|
||||
[disabled]="waitForResponse || formGroups.invalid"
|
||||
(click)="save()">
|
||||
<button mat-icon-button
|
||||
class="save"
|
||||
[disabled]="waitForResponse || formGroups.invalid"
|
||||
(click)="save()">
|
||||
<mat-icon>save</mat-icon>
|
||||
</button>
|
||||
@if (redemption.id) {
|
||||
<button
|
||||
mat-icon-button
|
||||
class="delete"
|
||||
[disabled]="waitForResponse"
|
||||
(click)="delete()">
|
||||
<button mat-icon-button
|
||||
class="delete"
|
||||
[disabled]="waitForResponse"
|
||||
(click)="delete()">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ button,
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
button ~ button {
|
||||
button~button {
|
||||
margin-left: 2em;
|
||||
}
|
||||
|
||||
|
@ -10,7 +10,7 @@ describe('RedemptionItemEditComponent', () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [RedemptionItemEditComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(RedemptionItemEditComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
@ -11,10 +11,10 @@ import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angula
|
||||
import { HermesClientService } from '../../hermes-client.service';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import TwitchRedemption from '../../shared/models/twitch-redemption';
|
||||
import RedeemableAction from '../../shared/models/redeemable_action';
|
||||
import RedeemableAction from '../../shared/models/redeemable-action';
|
||||
import { integerValidator } from '../../shared/validators/integer';
|
||||
import { createTypeValidator } from '../../shared/validators/of-type';
|
||||
import { RedemptionService } from '../../shared/services/redemption.service';
|
||||
import RedemptionService from '../../shared/services/redemption.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
|
@ -1,46 +1,68 @@
|
||||
<button mat-button class="add" (click)="add()"><mat-icon>add</mat-icon> Add Redemption</button>
|
||||
<div class="content">
|
||||
<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)" (closed)="panelOpenState.set(false)">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>Filters</mat-panel-title>
|
||||
<mat-panel-description>
|
||||
Expand for filtering options
|
||||
</mat-panel-description>
|
||||
</mat-expansion-panel-header>
|
||||
<mat-expansion-panel class="filters-expander"
|
||||
(opened)="panelOpenState.set(true)"
|
||||
(closed)="panelOpenState.set(false)">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>Filters</mat-panel-title>
|
||||
<mat-panel-description>
|
||||
{{!panelOpenState() ? 'Expand for filtering options' : ''}}
|
||||
</mat-panel-description>
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
<div class="filters">
|
||||
<twitch-redemption-dropdown [(twitchRedemptionId)]="filter_redemption" [search]="true" />
|
||||
<action-dropdown [(action)]="filter_action_name" [search]="true" />
|
||||
<div class="filters">
|
||||
<twitch-redemption-dropdown [(twitchRedemptionId)]="filter_redemption"
|
||||
[search]="true" />
|
||||
<action-dropdown [(action)]="filter_action_name"
|
||||
[search]="true" />
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
|
||||
<div class="table-container">
|
||||
<table mat-table
|
||||
[dataSource]="redemptions"
|
||||
class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="twitch-redemption">
|
||||
<th mat-header-cell
|
||||
*matHeaderCellDef>Twitch Redemption Name</th>
|
||||
<td mat-cell
|
||||
*matCellDef="let redemption">{{getTwitchRedemptionNameById(redemption.redemption_id) || 'Unknown
|
||||
Twitch Redemption'}}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="action-name">
|
||||
<th mat-header-cell
|
||||
*matHeaderCellDef>Action Name</th>
|
||||
<td mat-cell
|
||||
*matCellDef="let redemption">{{redemption.action_name}}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="order">
|
||||
<th mat-header-cell
|
||||
*matHeaderCellDef>Order</th>
|
||||
<td mat-cell
|
||||
*matCellDef="let redemption">{{redemption.order}}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="misc">
|
||||
<th mat-header-cell
|
||||
*matHeaderCellDef></th>
|
||||
<td mat-cell
|
||||
*matCellDef="let redemption">
|
||||
<button mat-icon-button
|
||||
(click)="openDialog(redemption)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row
|
||||
*matHeaderRowDef="displayedColumns; sticky: true"></tr>
|
||||
<tr mat-row
|
||||
*matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
|
||||
<div class="table-container">
|
||||
<table mat-table [dataSource]="redemptions" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="twitch-redemption">
|
||||
<th mat-header-cell *matHeaderCellDef>Twitch Redemption Name</th>
|
||||
<td mat-cell *matCellDef="let redemption">{{getTwitchRedemptionNameById(redemption.redemption_id) || 'Unknown Twitch Redemption'}}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="action-name">
|
||||
<th mat-header-cell *matHeaderCellDef>Action Name</th>
|
||||
<td mat-cell *matCellDef="let redemption">{{redemption.action_name}}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="order">
|
||||
<th mat-header-cell *matHeaderCellDef>Order</th>
|
||||
<td mat-cell *matCellDef="let redemption">{{redemption.order}}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="misc">
|
||||
<th mat-header-cell *matHeaderCellDef></th>
|
||||
<td mat-cell *matCellDef="let redemption">
|
||||
<button mat-icon-button (click)="openDialog(redemption)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
</div>
|
@ -1,22 +1,5 @@
|
||||
.filters-expander {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.mat-mdc-table {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
min-width: 555px;
|
||||
height: 60vh;
|
||||
overflow: auto;
|
||||
.content {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@ -24,4 +7,24 @@
|
||||
.add {
|
||||
width: 100%;
|
||||
margin-top: 3em;
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.filters-expander {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
min-width: 800px;
|
||||
flex: 1;
|
||||
height: 60vh;
|
||||
overflow: auto;
|
||||
margin-bottom: 2em;
|
||||
border-radius: 15px;
|
||||
}
|
@ -10,7 +10,7 @@ describe('RedemptionListComponent', () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [RedemptionListComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(RedemptionListComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Component, inject, OnDestroy, OnInit, signal } from '@angular/core';
|
||||
import { RedemptionService } from '../../shared/services/redemption.service';
|
||||
import RedemptionService from '../../shared/services/redemption.service';
|
||||
import Redemption from '../../shared/models/redemption';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { TwitchRedemptionDropdownComponent } from "../twitch-redemption-dropdown/twitch-redemption-dropdown.component";
|
||||
@ -13,7 +13,7 @@ import { MatTableModule } from '@angular/material/table';
|
||||
import TwitchRedemption from '../../shared/models/twitch-redemption';
|
||||
import { RedemptionItemEditComponent } from '../redemption-item-edit/redemption-item-edit.component';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import RedeemableAction from '../../shared/models/redeemable_action';
|
||||
import RedeemableAction from '../../shared/models/redeemable-action';
|
||||
import { MatExpansionModule } from '@angular/material/expansion';
|
||||
import { ScrollingModule } from '@angular/cdk/scrolling';
|
||||
import { Subscription } from 'rxjs';
|
||||
@ -48,7 +48,7 @@ export class RedemptionListComponent implements OnDestroy {
|
||||
displayedColumns: string[] = ['twitch-redemption', 'action-name', 'order', 'misc'];
|
||||
filter_redemption: string | undefined;
|
||||
filter_action_name: string | undefined;
|
||||
readonly panelOpenState = signal(true);
|
||||
readonly panelOpenState = signal(false);
|
||||
private _subscriptions: Subscription[] = []
|
||||
|
||||
|
||||
@ -64,9 +64,8 @@ export class RedemptionListComponent implements OnDestroy {
|
||||
});
|
||||
|
||||
let subscription = this.redemptionService.create$?.subscribe(d => {
|
||||
if (d.error || !d.data || d.request.nounce != null && d.request.nounce.startsWith(this.client.session_id)) {
|
||||
if (d.error || !d.data || d.request.nounce != null && d.request.nounce.startsWith(this.client.session_id))
|
||||
return;
|
||||
}
|
||||
|
||||
let index = -1;
|
||||
for (let i = 0; i < this._redemptions.length; i++) {
|
||||
@ -106,6 +105,10 @@ export class RedemptionListComponent implements OnDestroy {
|
||||
this._subscriptions.push(subscription);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.panelOpenState.set(false);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this._subscriptions.forEach(s => s.unsubscribe());
|
||||
}
|
||||
|
@ -1,9 +1,8 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RedemptionListComponent } from './redemption-list/redemption-list.component';
|
||||
import { RedemptionsComponent } from './redemptions/redemptions.component';
|
||||
import { TwitchRedemptionDropdownComponent } from './twitch-redemption-dropdown/twitch-redemption-dropdown.component';
|
||||
import { RedemptionService } from '../shared/services/redemption.service';
|
||||
import RedemptionService from '../shared/services/redemption.service';
|
||||
|
||||
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user