Compare commits
16 Commits
e053529d49
...
master
Author | SHA1 | Date | |
---|---|---|---|
9338e7e624 | |||
daa500111c | |||
b0f9a2dea8 | |||
931046cbb3 | |||
01c62bc143 | |||
f4511157a5 | |||
f2c5178e82 | |||
7048a7c46c | |||
0a511f1424 | |||
b8a92534d9 | |||
3e9a9f9dc5 | |||
70e0e9bf71 | |||
b465f0a474 | |||
fcf1e9ac03 | |||
1e6690ff4b | |||
5489eb4df6 |
@ -47,8 +47,8 @@
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "1024kB",
|
||||
"maximumError": "1MB"
|
||||
"maximumWarning": "3MB",
|
||||
"maximumError": "5MB"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
@ -93,7 +93,7 @@
|
||||
},
|
||||
"defaultConfiguration": "development",
|
||||
"options": {
|
||||
"allowedHosts": ["*"]
|
||||
"allowedHosts": ["beta.tomtospeech.com"]
|
||||
}
|
||||
},
|
||||
"extract-i18n": {
|
||||
|
10
package-lock.json
generated
10
package-lock.json
generated
@ -22,6 +22,7 @@
|
||||
"@angular/ssr": "^19.2.5",
|
||||
"angular-oauth2-oidc": "^17.0.2",
|
||||
"express": "^4.18.2",
|
||||
"moment": "^2.30.1",
|
||||
"ngx-socket-io": "^4.7.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"rxjs-websockets": "^9.0.0",
|
||||
@ -10344,6 +10345,15 @@
|
||||
"mkdirp": "bin/cmd.js"
|
||||
}
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.30.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
||||
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/mrmime": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
|
||||
|
@ -2,8 +2,7 @@
|
||||
"name": "hermes-web-angular",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve -c production --host 0.0.0.0 --watch false",
|
||||
"start": "ng serve -c development --host 0.0.0.0 --watch false",
|
||||
"build": "ng build",
|
||||
"watch": "ng serve -c development --host 0.0.0.0 --disable-host-check",
|
||||
"test": "ng test",
|
||||
@ -25,6 +24,7 @@
|
||||
"@angular/ssr": "^19.2.5",
|
||||
"angular-oauth2-oidc": "^17.0.2",
|
||||
"express": "^4.18.2",
|
||||
"moment": "^2.30.1",
|
||||
"ngx-socket-io": "^4.7.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"rxjs-websockets": "^9.0.0",
|
||||
|
@ -1,9 +1,8 @@
|
||||
import { Component, EventEmitter, inject, input, Input, OnInit, Output } from '@angular/core';
|
||||
import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { Component, EventEmitter, 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 RedeemableAction from '../../shared/models/redeemable-action';
|
||||
|
||||
@Component({
|
||||
@ -21,20 +20,9 @@ export class ActionDropdownComponent implements OnInit {
|
||||
@Input() action: string | undefined;
|
||||
@Output() readonly actionChange = new EventEmitter<string>();
|
||||
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
|
||||
errorMessageKeys: string[] = []
|
||||
|
||||
|
||||
constructor() {
|
||||
this.route.data.subscribe(data => {
|
||||
if (!data['redeemableActions'])
|
||||
return;
|
||||
|
||||
this.actions = data['redeemableActions'];
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.errorMessageKeys = Object.keys(this.errorMessages);
|
||||
|
||||
|
@ -28,10 +28,12 @@ import { MatIconModule } from '@angular/material/icon';
|
||||
})
|
||||
export class ActionItemEditComponent implements OnInit {
|
||||
private readonly client = inject(HermesClientService);
|
||||
readonly dialogRef = inject(MatDialogRef<ActionItemEditComponent>);
|
||||
private readonly data = inject<{ action: RedeemableAction, actions: RedeemableAction[] }>(MAT_DIALOG_DATA);
|
||||
readonly dialogRef = inject(MatDialogRef<ActionItemEditComponent>);
|
||||
|
||||
action = this.data.action;
|
||||
actions = this.data.actions;
|
||||
|
||||
readonly actionEntries: ({ [key: string]: any[] }) = {
|
||||
'SLEEP': [
|
||||
{
|
||||
@ -259,7 +261,7 @@ export class ActionItemEditComponent implements OnInit {
|
||||
}
|
||||
|
||||
save(): void {
|
||||
if (this.formGroup.invalid || this.waitForResponse) {
|
||||
if (!this.formGroup.dirty || this.formGroup.invalid || this.waitForResponse) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -272,18 +274,18 @@ export class ActionItemEditComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
this.action.name = this.formGroup.get('name')!.value!;
|
||||
this.action.type = this.formGroup.get('type')!.value!;
|
||||
this.action.data = {}
|
||||
for (const entry of this.actionEntries[this.action.type]) {
|
||||
this.action.data[entry.key] = entry.control.value!.toString();
|
||||
}
|
||||
|
||||
if (!(this.action.type in this.actionEntries)) {
|
||||
this.waitForResponse = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.action.name = this.formGroup.get('name')!.value!;
|
||||
this.action.type = this.formGroup.get('type')!.value!;
|
||||
this.action.data = {};
|
||||
for (const entry of this.actionEntries[this.action.type]) {
|
||||
this.action.data[entry.key] = entry.control.value!.toString();
|
||||
}
|
||||
|
||||
const isNewAction = !this.action.user_id;
|
||||
const requestType = isNewAction ? 'create_redeemable_action' : 'update_redeemable_action';
|
||||
this.client.first((d: any) => d.op == 4 && d.d.request.type == requestType && d.d.data.name == this.action.name)
|
||||
@ -299,8 +301,8 @@ export class ActionItemEditComponent implements OnInit {
|
||||
complete: () => this.waitForResponse = false,
|
||||
});
|
||||
if (isNewAction)
|
||||
this.client.createRedeemableAction(this.action.name, this.action.type, this.action.data);
|
||||
this.client.createRedeemableAction(this.action.name, this.action.type, false, this.action.data);
|
||||
else
|
||||
this.client.updateRedeemableAction(this.action.name, this.action.type, this.action.data);
|
||||
this.client.updateRedeemableAction(this.action.name, this.action.type, false, this.action.data);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
<main>
|
||||
@for (action of actions; track action.name) {
|
||||
@for (action of actions(); track action.name) {
|
||||
<button type="button"
|
||||
class="container"
|
||||
(click)="modify(action)">
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, EventEmitter, inject, Input, Output } from '@angular/core';
|
||||
import { Component, inject, input } from '@angular/core';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import RedeemableAction from '../../shared/models/redeemable-action';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
@ -23,16 +23,14 @@ import { MatSelectModule } from '@angular/material/select';
|
||||
styleUrl: './action-list.component.scss'
|
||||
})
|
||||
export class ActionListComponent {
|
||||
@Input() actions: RedeemableAction[] = []
|
||||
@Output() actionsChange = new EventEmitter<RedeemableAction>();
|
||||
actions = input.required<RedeemableAction[]>({ alias: 'actions' });
|
||||
|
||||
readonly dialog = inject(MatDialog);
|
||||
readonly client = inject(HermesClientService);
|
||||
|
||||
opened = false;
|
||||
|
||||
create(): void {
|
||||
this.openDialog({ user_id: '', name: '', type: '', data: {} });
|
||||
this.openDialog({ user_id: '', name: '', type: '', has_message: false, data: {} });
|
||||
}
|
||||
|
||||
modify(action: RedeemableAction): void {
|
||||
@ -40,27 +38,8 @@ export class ActionListComponent {
|
||||
}
|
||||
|
||||
private openDialog(action: RedeemableAction): void {
|
||||
if (this.opened)
|
||||
return;
|
||||
|
||||
this.opened = true;
|
||||
|
||||
const dialogRef = this.dialog.open(ActionItemEditComponent, {
|
||||
data: { action: { user_id: action.user_id, name: action.name, type: action.type, data: action.data }, actions: this.actions },
|
||||
});
|
||||
|
||||
const isNewAction = action.name.length <= 0;
|
||||
dialogRef.afterClosed().subscribe((result: RedeemableAction | undefined) => {
|
||||
this.opened = false;
|
||||
if (!result)
|
||||
return;
|
||||
|
||||
if (isNewAction) {
|
||||
this.actionsChange.emit(result);
|
||||
} else {
|
||||
action.type = result.type;
|
||||
action.data = result.data;
|
||||
}
|
||||
this.dialog.open(ActionItemEditComponent, {
|
||||
data: { action: { user_id: action.user_id, name: action.name, type: action.type, data: action.data }, actions: this.actions() },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -3,10 +3,10 @@
|
||||
|
||||
<section>
|
||||
<article>
|
||||
<mat-form-field>
|
||||
<mat-form-field subscriptSizing="dynamic">
|
||||
<mat-label>Filter by type</mat-label>
|
||||
<mat-select (selectionChange)="onFilterChange($event.value)"
|
||||
value="0">
|
||||
<mat-select value="0"
|
||||
(selectionChange)="filter = filters[$event.value]">
|
||||
<mat-select-trigger>
|
||||
<mat-icon matPrefix>filter_list</mat-icon> {{filter.name}}
|
||||
</mat-select-trigger>
|
||||
@ -17,18 +17,16 @@
|
||||
</mat-form-field>
|
||||
</article>
|
||||
<article>
|
||||
<mat-form-field>
|
||||
<mat-form-field subscriptSizing="dynamic">
|
||||
<mat-label>Search</mat-label>
|
||||
<input matInput
|
||||
type="text"
|
||||
placeholder="Name of action"
|
||||
[formControl]="searchControl"
|
||||
[(ngModel)]="search">
|
||||
[formControl]="searchControl" />
|
||||
<mat-icon matPrefix>search</mat-icon>
|
||||
</mat-form-field>
|
||||
</article>
|
||||
</section>
|
||||
<action-list class="center"
|
||||
[actions]="actions"
|
||||
(actionsChange)="items.push($event)" />
|
||||
<action-list class="list center"
|
||||
[actions]="actions" />
|
||||
</content>
|
@ -1,12 +1,12 @@
|
||||
body,
|
||||
content,
|
||||
h3 {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 3em;
|
||||
}
|
||||
|
||||
@ -19,8 +19,7 @@ section {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 70%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin: 0 auto;
|
||||
|
||||
@media (max-width:1250px) {
|
||||
display: block;
|
||||
@ -30,6 +29,7 @@ section {
|
||||
article {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin: 1em 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, Inject, inject, OnInit } from '@angular/core';
|
||||
import { Component, inject, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ActionListComponent } from "../action-list/action-list.component";
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
@ -10,7 +10,8 @@ import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import RedeemableActionService from '../../shared/services/redeemable-action.service';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { containsLettersInOrder } from '../../shared/utils/string-compare';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
interface IActionFilter {
|
||||
name: string
|
||||
@ -32,7 +33,7 @@ interface IActionFilter {
|
||||
templateUrl: './actions.component.html',
|
||||
styleUrl: './actions.component.scss'
|
||||
})
|
||||
export class ActionsComponent implements OnInit {
|
||||
export class ActionsComponent implements OnInit, OnDestroy {
|
||||
filters: IActionFilter[] = [
|
||||
{ name: 'All', filter: _ => true },
|
||||
{ name: 'Local File', filter: data => data.type.includes('_FILE') },
|
||||
@ -47,58 +48,28 @@ export class ActionsComponent implements OnInit {
|
||||
private readonly redeemableActionService = inject(RedeemableActionService);
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
|
||||
filter = this.filters[0];
|
||||
searchControl = new FormControl('');
|
||||
search = '';
|
||||
items: RedeemableAction[] = [];
|
||||
private readonly subscriptions: (Subscription | undefined)[] = [];
|
||||
|
||||
constructor(@Inject(DOCUMENT) private document: Document) { }
|
||||
filter = this.filters[0];
|
||||
searchControl = new FormControl<string>('');
|
||||
_actions: RedeemableAction[] = [];
|
||||
|
||||
|
||||
ngOnInit(): void {
|
||||
this.route.data.subscribe(data => {
|
||||
if (!data['redeemableActions'])
|
||||
return;
|
||||
this.route.data.subscribe(data => this._actions = data['redeemableActions'] || []);
|
||||
|
||||
this.actions = [...data['redeemableActions']];
|
||||
});
|
||||
this.redeemableActionService.create$?.subscribe(d => {
|
||||
if (d.error || d.request.nounce != null && d.request.nounce.startsWith(this.client.session_id))
|
||||
return;
|
||||
|
||||
this.actions.push(d.data);
|
||||
});
|
||||
this.redeemableActionService.update$?.subscribe(d => {
|
||||
if (d.error || d.request.nounce != null && d.request.nounce.startsWith(this.client.session_id))
|
||||
return;
|
||||
|
||||
const action = this.actions.find(a => a.name == d.data.name);
|
||||
if (action) {
|
||||
action.type = d.data.type;
|
||||
action.data = d.data.data;
|
||||
}
|
||||
});
|
||||
this.redeemableActionService.delete$?.subscribe(d => {
|
||||
if (d.error)
|
||||
return;
|
||||
|
||||
this.items = this.actions.filter(a => a.name != d.request.data.name);
|
||||
});
|
||||
this.subscriptions.push(this.redeemableActionService.changes$?.subscribe(a => this._actions = a));
|
||||
|
||||
this.client.fetchRedeemableActions();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subscriptions.filter(s => s).forEach(s => s?.unsubscribe());
|
||||
}
|
||||
|
||||
get actions(): RedeemableAction[] {
|
||||
const searchLower = this.search.toLowerCase();
|
||||
return this.items.filter(this.filter.filter)
|
||||
.filter((action) => action.name.toLowerCase().includes(searchLower));
|
||||
}
|
||||
|
||||
set actions(value) {
|
||||
this.items = value;
|
||||
}
|
||||
|
||||
onFilterChange(event: any): void {
|
||||
this.filter = this.filters[event];
|
||||
const searchLower = this.searchControl.value!.toLowerCase();
|
||||
return this._actions.filter(this.filter.filter)
|
||||
.filter((action) => containsLettersInOrder(action.name.toLowerCase(), searchLower));
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
<main>
|
||||
<topbar class="top" />
|
||||
<div [class.container]="isSidebarOpen"
|
||||
<div class="below-topbar"
|
||||
[class.grid]="isSidebarOpen"
|
||||
[class.full]="!isSidebarOpen">
|
||||
@if (isSidebarOpen) {
|
||||
<sidebar class="navigation" />
|
||||
|
@ -1,9 +1,8 @@
|
||||
.container {
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 20em 0px 1fr;
|
||||
}
|
||||
|
||||
.full {
|
||||
width: 80%;
|
||||
margin: 0 auto;
|
||||
}
|
@ -8,7 +8,6 @@ import EventService from './shared/services/EventService';
|
||||
import { ApiAuthenticationService } from './shared/services/api/api-authentication.service';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
import { ApiKeyService } from './shared/services/api/api-key.service';
|
||||
import ApiKey from './shared/models/api-key';
|
||||
import { ThemeService } from './shared/services/theme.service';
|
||||
import { OverlayContainer } from '@angular/cdk/overlay';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
@ -16,6 +15,7 @@ import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { SidebarComponent } from "./navigation/sidebar/sidebar.component";
|
||||
import { Topbar as TopbarComponent } from "./navigation/topbar/topbar.component";
|
||||
import ApiKey from './shared/models/api-key';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@ -38,7 +38,6 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
private readonly overlayContainer = inject(OverlayContainer);
|
||||
private readonly themeService = inject(ThemeService);
|
||||
|
||||
private isBrowser: boolean;
|
||||
private ngZone: NgZone;
|
||||
private subscriptions: Subscription[];
|
||||
|
||||
@ -57,7 +56,6 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
|
||||
constructor(private auth: ApiAuthenticationService, private client: HermesClientService, private events: EventService, private router: Router, ngZone: NgZone, @Inject(PLATFORM_ID) private platformId: Object) {
|
||||
this.ngZone = ngZone;
|
||||
this.isBrowser = isPlatformBrowser(this.platformId);
|
||||
this.subscriptions = [];
|
||||
|
||||
this.subscriptions.push(this.events.listen('tts_login_ack', async _ => {
|
||||
@ -72,15 +70,26 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}));
|
||||
|
||||
this.addSubscription(this.events.listen('login', () => {
|
||||
this.keyService.fetch()
|
||||
.pipe(timeout(3000), first())
|
||||
.subscribe(async (d: ApiKey[]) => {
|
||||
if (d.length > 0)
|
||||
this.client.login(d[0].id);
|
||||
});
|
||||
}));
|
||||
|
||||
this.subscriptions.push(this.events.listen('tts_logoff', async _ => await this.router.navigate(['tts-login'])));
|
||||
this.subscriptions.push(this.events.listen('toggle_sidebar', () => this.isSidebarOpen = !this.isSidebarOpen))
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (!this.isBrowser)
|
||||
if (!isPlatformBrowser(this.platformId))
|
||||
return;
|
||||
|
||||
this.auth.update();
|
||||
this.auth.update(localStorage.getItem('jwt'));
|
||||
|
||||
this.subscriptions.push(this.events.listen('login', async () => await this.router.navigate(['tts-login'])));
|
||||
|
||||
this.addSubscription(this.events.listen('logoff', async (message) => {
|
||||
localStorage.removeItem('jwt');
|
||||
@ -94,27 +103,30 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}));
|
||||
|
||||
this.addSubscription(this.events.listen('login', () => {
|
||||
this.keyService.fetch()
|
||||
.pipe(timeout(3000), first())
|
||||
.subscribe(async (d: ApiKey[]) => {
|
||||
if (d.length > 0)
|
||||
this.client.login(d[0].id);
|
||||
else if (['/login', '/auth'].some(partial => document.location.href.includes(partial)))
|
||||
await this.router.navigate(['tts-login']);
|
||||
});
|
||||
}));
|
||||
|
||||
let currentTheme = localStorage.getItem('ui-theme') ?? this.themeService.theme;
|
||||
if (currentTheme == 'light' || currentTheme == 'dark') {
|
||||
this.themeService.theme = currentTheme;
|
||||
} else {
|
||||
this.themeService.theme = 'dark';
|
||||
}
|
||||
this.overlayContainer.getContainerElement().classList.add(this.themeService.theme + '-theme');
|
||||
|
||||
this.addSubscription(this.events.listen('theme_change', data => {
|
||||
const classList = this.overlayContainer.getContainerElement().classList;
|
||||
classList.remove(data.previous_theme + '-theme');
|
||||
classList.add(data.current_theme + '-theme');
|
||||
}));
|
||||
|
||||
this.ngZone.runOutsideAngular(() => setInterval(() => this.client.heartbeat(), 15000));
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
for (let s of this.subscriptions) {
|
||||
if (s) {
|
||||
s.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private addSubscription(s: Subscription) {
|
||||
this.subscriptions.push(s);
|
||||
|
@ -1,14 +1,12 @@
|
||||
@if (isAdmin()) {
|
||||
<main>
|
||||
<mat-form-field class="mat-small"
|
||||
subscriptSizing="dynamic">
|
||||
<mat-label>User to impersonate</mat-label>
|
||||
<mat-select [formControl]="impersonationControl">
|
||||
<mat-option>{{getUsername()}}</mat-option>
|
||||
@for (user of users; track user.id) {
|
||||
<mat-option [value]="auth.getUserId()">{{getUsername()}}</mat-option>
|
||||
@for (user of (users$ | async | excludeById : auth.getUserId()); track user.id) {
|
||||
<mat-option [value]="user.id">{{ user.name }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</main>
|
||||
}
|
@ -1,21 +1,22 @@
|
||||
import { Component, inject, Inject, OnInit, PLATFORM_ID } from '@angular/core';
|
||||
import { Component, inject, OnInit } from '@angular/core';
|
||||
import { ApiAuthenticationService } from '../../shared/services/api/api-authentication.service';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { isPlatformBrowser } from '@angular/common';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import EventService from '../../shared/services/EventService';
|
||||
import { HermesClientService } from '../../hermes-client.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||
import { User } from '../../shared/models/user';
|
||||
import { UserService } from '../../shared/services/user.service';
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import { ExcludeByIdPipe } from '../../shared/pipes/exclude-by-id.pipe';
|
||||
|
||||
@Component({
|
||||
selector: 'impersonation',
|
||||
standalone: true,
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
ExcludeByIdPipe,
|
||||
MatCardModule,
|
||||
MatSelectModule,
|
||||
ReactiveFormsModule,
|
||||
@ -24,34 +25,33 @@ import { UserService } from '../../shared/services/user.service';
|
||||
styleUrl: './impersonation.component.scss'
|
||||
})
|
||||
export class ImpersonationComponent implements OnInit {
|
||||
private readonly events = inject(EventService);
|
||||
private readonly client = inject(HermesClientService);
|
||||
private readonly userService = inject(UserService);
|
||||
private readonly events = inject(EventService);
|
||||
private readonly http = inject(HttpClient);
|
||||
|
||||
impersonationControl = new FormControl<string | undefined>(undefined);
|
||||
users: User[];
|
||||
readonly auth = inject(ApiAuthenticationService);
|
||||
|
||||
constructor(private client: HermesClientService, private auth: ApiAuthenticationService, private router: Router, private http: HttpClient, @Inject(PLATFORM_ID) private platformId: Object) {
|
||||
this.users = [];
|
||||
}
|
||||
impersonationControl = new FormControl<string>(this.auth.getUserId());
|
||||
users$ = this.userService.fetch();
|
||||
|
||||
ngOnInit(): void {
|
||||
if (!isPlatformBrowser(this.platformId)) {
|
||||
if (!this.auth.isAdmin()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.userService.fetch().subscribe(users => {
|
||||
this.users = users.filter((d: any) => d.name != this.auth.getUsername());
|
||||
this.users$.subscribe(users => {
|
||||
const id = this.auth.getImpersonatedId();
|
||||
if (id && this.users.find(u => u.id == id)) {
|
||||
if (id && users.find(u => u.id == id)) {
|
||||
this.impersonationControl.setValue(id);
|
||||
}
|
||||
});
|
||||
|
||||
this.impersonationControl.valueChanges.subscribe((impersonationId) => {
|
||||
if (!this.auth.isAdmin() || impersonationId == this.auth.getImpersonatedId())
|
||||
if (impersonationId == this.auth.getImpersonatedId())
|
||||
return;
|
||||
|
||||
if (!impersonationId) {
|
||||
if (impersonationId == this.auth.getUserId()) {
|
||||
this.http.delete(environment.API_HOST + '/admin/impersonate', {
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('jwt')
|
||||
@ -60,9 +60,8 @@ export class ImpersonationComponent implements OnInit {
|
||||
impersonation: impersonationId
|
||||
}
|
||||
}).subscribe(async (data: any) => {
|
||||
this.impersonationControl.setValue(undefined);
|
||||
this.client.disconnect(true);
|
||||
this.events.emit('impersonation', undefined);
|
||||
this.events.emit('impersonation', impersonationId);
|
||||
});
|
||||
} else {
|
||||
this.http.put(environment.API_HOST + '/admin/impersonate', {
|
||||
@ -72,10 +71,8 @@ export class ImpersonationComponent implements OnInit {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('jwt')
|
||||
}
|
||||
}).subscribe(async (data: any) => {
|
||||
this.impersonationControl.setValue(impersonationId);
|
||||
this.client.disconnect(true);
|
||||
this.events.emit('impersonation', impersonationId);
|
||||
await this.router.navigate(['tts-login']);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
6
src/app/auth/login-button/login-button.component.html
Normal file
6
src/app/auth/login-button/login-button.component.html
Normal file
@ -0,0 +1,6 @@
|
||||
<button mat-icon-button
|
||||
class="neutral"
|
||||
matTooltip="Navigate to the log in page"
|
||||
(click)="login()">
|
||||
<mat-icon>login</mat-icon>
|
||||
</button>
|
23
src/app/auth/login-button/login-button.component.spec.ts
Normal file
23
src/app/auth/login-button/login-button.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { LoginButtonComponent } from './login-button.component';
|
||||
|
||||
describe('LoginButtonComponent', () => {
|
||||
let component: LoginButtonComponent;
|
||||
let fixture: ComponentFixture<LoginButtonComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [LoginButtonComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(LoginButtonComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
28
src/app/auth/login-button/login-button.component.ts
Normal file
28
src/app/auth/login-button/login-button.component.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'login-button',
|
||||
standalone: true,
|
||||
imports: [
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
MatFormFieldModule,
|
||||
MatTooltipModule,
|
||||
ReactiveFormsModule,
|
||||
],
|
||||
templateUrl: './login-button.component.html',
|
||||
styleUrl: './login-button.component.scss'
|
||||
})
|
||||
export class LoginButtonComponent {
|
||||
private readonly router = inject(Router);
|
||||
|
||||
login() {
|
||||
this.router.navigate(['login']);
|
||||
}
|
||||
}
|
6
src/app/auth/logoff-button/logoff-button.component.html
Normal file
6
src/app/auth/logoff-button/logoff-button.component.html
Normal file
@ -0,0 +1,6 @@
|
||||
<button mat-icon-button
|
||||
class="danger"
|
||||
matTooltip="Log off"
|
||||
(click)="logoff()">
|
||||
<mat-icon>logout</mat-icon>
|
||||
</button>
|
23
src/app/auth/logoff-button/logoff-button.component.spec.ts
Normal file
23
src/app/auth/logoff-button/logoff-button.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { LogoffButtonComponent } from './logoff-button.component';
|
||||
|
||||
describe('LogoffButtonComponent', () => {
|
||||
let component: LogoffButtonComponent;
|
||||
let fixture: ComponentFixture<LogoffButtonComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [LogoffButtonComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(LogoffButtonComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
28
src/app/auth/logoff-button/logoff-button.component.ts
Normal file
28
src/app/auth/logoff-button/logoff-button.component.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { ApiAuthenticationService } from '../../shared/services/api/api-authentication.service';
|
||||
|
||||
@Component({
|
||||
selector: 'logoff-button',
|
||||
standalone: true,
|
||||
imports: [
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
MatFormFieldModule,
|
||||
MatTooltipModule,
|
||||
ReactiveFormsModule,
|
||||
],
|
||||
templateUrl: './logoff-button.component.html',
|
||||
styleUrl: './logoff-button.component.scss'
|
||||
})
|
||||
export class LogoffButtonComponent {
|
||||
private readonly auth = inject(ApiAuthenticationService);
|
||||
|
||||
logoff() {
|
||||
this.auth.logout();
|
||||
}
|
||||
}
|
@ -18,6 +18,7 @@
|
||||
</mat-card-content>
|
||||
<mat-card-actions>
|
||||
<button mat-raised-button
|
||||
[disabled]="disabled"
|
||||
(click)="login()">Log In</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, inject, OnInit } from '@angular/core';
|
||||
import { Component, inject, OnDestroy, OnInit } from '@angular/core';
|
||||
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
@ -6,6 +6,9 @@ import { MatButtonModule } from '@angular/material/button';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { HermesClientService } from '../../hermes-client.service';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import EventService from '../../shared/services/EventService';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { ApiKeyService } from '../../shared/services/api/api-key.service';
|
||||
|
||||
@Component({
|
||||
selector: 'tts-login',
|
||||
@ -20,16 +23,34 @@ import { MatCardModule } from '@angular/material/card';
|
||||
templateUrl: './tts-login.component.html',
|
||||
styleUrl: './tts-login.component.scss'
|
||||
})
|
||||
export class TtsLoginComponent implements OnInit {
|
||||
export class TtsLoginComponent implements OnInit, OnDestroy {
|
||||
private readonly client = inject(HermesClientService);
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly keyService = inject(ApiKeyService);
|
||||
private readonly eventService = inject(EventService);
|
||||
|
||||
keyControl = new FormControl<string | null>('');
|
||||
api_keys: { id: string, label: string }[] = [];
|
||||
subscriptions: (Subscription | null)[] = [];
|
||||
disabled: boolean = false;
|
||||
|
||||
|
||||
ngOnInit(): void {
|
||||
this.route.data.subscribe(d => this.api_keys = d['keys']);
|
||||
|
||||
this.subscriptions.push(this.eventService.listen('impersonation', _ => this.reset()));
|
||||
this.subscriptions.push(this.eventService.listen('logoff', impersonation => {
|
||||
if (!impersonation)
|
||||
this.reset();
|
||||
}));
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
for (let subscription of this.subscriptions) {
|
||||
if (subscription) {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
login(): void {
|
||||
@ -38,4 +59,13 @@ export class TtsLoginComponent implements OnInit {
|
||||
|
||||
this.client.login(this.keyControl.value);
|
||||
}
|
||||
|
||||
private reset() {
|
||||
this.disabled = true;
|
||||
this.api_keys = [];
|
||||
this.keyService.fetch().subscribe(keys => {
|
||||
this.api_keys = keys;
|
||||
this.disabled = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { environment } from '../../../environments/environment';
|
||||
|
||||
@Component({
|
||||
selector: 'connection-item-edit',
|
||||
@ -50,11 +51,11 @@ export class ConnectionItemEditComponent {
|
||||
}
|
||||
|
||||
submit(): void {
|
||||
if (this.form.invalid || this.waitForResponse) {
|
||||
if (!this.form.dirty || this.form.invalid || this.waitForResponse) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.http.post('/api/auth/connections', {
|
||||
this.http.post(environment.API_HOST + '/auth/connections', {
|
||||
name: this.nameControl.value,
|
||||
type: this.typeControl.value,
|
||||
client_id: this.clientIdControl.value,
|
||||
|
@ -1,7 +1,15 @@
|
||||
<section [class.twitch]="connection().type == 'twitch'"
|
||||
[class.spotify]="connection().type == 'spotify'">
|
||||
[class.nightbot]="connection().type == 'nightbot'">
|
||||
{{connection().name}}
|
||||
|
||||
@if (isExpired) {
|
||||
<mat-icon matTooltip="Connection has expired."
|
||||
class="danger">error</mat-icon>
|
||||
} @else if (isExpiringSoon) {
|
||||
<mat-icon matTooltip="Connection is soon going to expire."
|
||||
class="warning">warning</mat-icon>
|
||||
}
|
||||
|
||||
<article class="right">
|
||||
<button mat-button
|
||||
class="neutral"
|
||||
|
@ -2,6 +2,14 @@ section {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.twitch {
|
||||
border-left: 1em solid #6441A5;
|
||||
}
|
||||
|
||||
.nightbot {
|
||||
border-left: 1em solid #3D5D9A;
|
||||
}
|
||||
|
||||
.right {
|
||||
float: right;
|
||||
}
|
||||
|
@ -3,10 +3,13 @@ import { Connection } from '../../shared/models/connection';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import {MatTooltipModule} from '@angular/material/tooltip';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { HermesClientService } from '../../hermes-client.service';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import moment from 'moment';
|
||||
|
||||
@Component({
|
||||
selector: 'connection-item',
|
||||
@ -14,6 +17,7 @@ import { HermesClientService } from '../../hermes-client.service';
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
MatFormFieldModule,
|
||||
MatTooltipModule,
|
||||
ReactiveFormsModule,
|
||||
],
|
||||
templateUrl: './connection-item.component.html',
|
||||
@ -31,9 +35,17 @@ export class ConnectionItemComponent {
|
||||
this.client.deleteConnection(this.connection().name);
|
||||
}
|
||||
|
||||
get isExpired() {
|
||||
return moment(this.connection().expires_at).toDate().getTime() < new Date().getTime();
|
||||
}
|
||||
|
||||
get isExpiringSoon() {
|
||||
return moment(this.connection().expires_at).toDate().getTime() < moment.now() + moment.duration(7, 'd').asMilliseconds();
|
||||
}
|
||||
|
||||
renew() {
|
||||
const conn = this.connection();
|
||||
this.http.post('/api/auth/connections', {
|
||||
this.http.post(environment.API_HOST + '/auth/connections', {
|
||||
name: conn.name,
|
||||
type: conn.type,
|
||||
client_id: conn.client_id,
|
||||
|
@ -1,6 +1,13 @@
|
||||
@use '@angular/material' as mat;
|
||||
|
||||
ul {
|
||||
background-color: rgb(202, 68, 255);
|
||||
border-radius: 15px;
|
||||
margin: 0 0;
|
||||
padding: 0;
|
||||
max-width: 500px;
|
||||
overflow: hidden;
|
||||
|
||||
@include mat.all-component-densities(-5);
|
||||
|
||||
@include mat.form-field-overrides((
|
||||
@ -8,13 +15,6 @@ ul {
|
||||
outlined-focus-label-text-color: rgb(155, 57, 194),
|
||||
outlined-focus-outline-color: rgb(155, 57, 194),
|
||||
));
|
||||
|
||||
background-color: rgb(202, 68, 255);
|
||||
border-radius: 15px;
|
||||
margin: 0 0;
|
||||
padding: 0;
|
||||
max-width: 500px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
ul li {
|
||||
|
@ -50,7 +50,7 @@ export class GroupItemEditComponent implements OnInit {
|
||||
}
|
||||
|
||||
add() {
|
||||
if (this.formGroup.invalid || this.waitForResponse)
|
||||
if (!this.formGroup.dirty || this.formGroup.invalid || this.waitForResponse)
|
||||
return;
|
||||
|
||||
this.waitForResponse = true;
|
||||
|
@ -37,7 +37,8 @@
|
||||
[groups]="groups"
|
||||
[policies]="policies"
|
||||
[group]="group?.id" />
|
||||
<policy-table [policies]="policies" />
|
||||
<policy-table [policies]="policies"
|
||||
[groups]="groups"/>
|
||||
</mat-expansion-panel>
|
||||
|
||||
<mat-expansion-panel>
|
||||
|
@ -156,14 +156,14 @@ export class HermesClientService {
|
||||
});
|
||||
}
|
||||
|
||||
public createRedeemableAction(name: string, type: string, d: { [key: string]: any }) {
|
||||
public createRedeemableAction(name: string, type: string, has_message: boolean, d: { [key: string]: any }) {
|
||||
if (!this.logged_in)
|
||||
return;
|
||||
|
||||
this.send(3, {
|
||||
request_id: null,
|
||||
type: "create_redeemable_action",
|
||||
data: { name, type, data: d },
|
||||
data: { name, type, has_message, data: d },
|
||||
nounce: this.session_id,
|
||||
});
|
||||
}
|
||||
@ -439,14 +439,14 @@ export class HermesClientService {
|
||||
});
|
||||
}
|
||||
|
||||
public updateRedeemableAction(name: string, type: string, d: { [key: string]: any }) {
|
||||
public updateRedeemableAction(name: string, type: string, has_message: boolean, d: { [key: string]: any }) {
|
||||
if (!this.logged_in)
|
||||
return;
|
||||
|
||||
this.send(3, {
|
||||
request_id: null,
|
||||
type: "update_redeemable_action",
|
||||
data: { name, type, data: d },
|
||||
data: { name, type, has_message, data: d },
|
||||
nounce: this.session_id,
|
||||
});
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { OnInit, Injectable } from '@angular/core';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
|
||||
import { catchError, first, timeout } from 'rxjs/operators';
|
||||
import { environment } from '../environments/environment';
|
||||
@ -8,13 +8,9 @@ import { EMPTY, Observable, Observer, throwError } from 'rxjs';
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class HermesSocketService implements OnInit {
|
||||
export class HermesSocketService {
|
||||
private socket: WebSocketSubject<any> | undefined = undefined
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
public connect(): void {
|
||||
if (!this.socket || this.socket.closed) {
|
||||
@ -22,9 +18,10 @@ export class HermesSocketService implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
public first(predicate: (data: any) => boolean): Observable<any> {
|
||||
if (!this.socket || this.socket.closed)
|
||||
return new Observable().pipe(timeout(3000), catchError((e) => throwError(() => 'No response after 3 seconds.')));
|
||||
public first<T>(predicate: (data: T) => boolean): Observable<T> {
|
||||
if (!this.socket || this.socket.closed) {
|
||||
throw new Error('Socket is ' + (this.socket ? 'closed' : 'null') + '.');
|
||||
}
|
||||
|
||||
return this.socket.pipe(timeout(3000), catchError((e) => throwError(() => 'No response after 3 seconds.')), first(predicate));
|
||||
}
|
||||
@ -43,7 +40,11 @@ export class HermesSocketService implements OnInit {
|
||||
}
|
||||
|
||||
public get$(): Observable<any> | undefined {
|
||||
return this.socket?.asObservable().pipe(catchError(_ => EMPTY));
|
||||
if (!this.socket || this.socket.closed) {
|
||||
throw new Error('Socket is ' + (this.socket ? 'closed' : 'null') + '.');
|
||||
}
|
||||
|
||||
return this.socket.asObservable().pipe(catchError(_ => EMPTY));
|
||||
}
|
||||
|
||||
public subscribe(subscriptions: Partial<Observer<any>> | ((value: any) => void)) {
|
||||
|
@ -9,6 +9,7 @@ import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import EventService from '../../shared/services/EventService';
|
||||
import { environment } from '../../../environments/environment';
|
||||
|
||||
@Component({
|
||||
selector: 'key-item-edit',
|
||||
@ -45,12 +46,12 @@ export class KeyItemEditComponent {
|
||||
}
|
||||
|
||||
submit(): void {
|
||||
if (this.form.invalid || this.waitForResponse) {
|
||||
if (!this.form.dirty || this.form.invalid || this.waitForResponse) {
|
||||
return;
|
||||
}
|
||||
|
||||
const label = this.labelControl.value;
|
||||
this.http.post('/api/keys', { label },
|
||||
this.http.post(environment.API_HOST + '/keys', { label },
|
||||
{
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('jwt')
|
||||
|
@ -6,6 +6,7 @@ import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import ApiKey from '../../shared/models/api-key';
|
||||
import EventService from '../../shared/services/EventService';
|
||||
import { environment } from '../../../environments/environment';
|
||||
|
||||
@Component({
|
||||
selector: 'key-item',
|
||||
@ -32,7 +33,7 @@ export class KeyItemComponent {
|
||||
}
|
||||
|
||||
const key_id = this.key().id;
|
||||
this.http.delete('/api/keys',
|
||||
this.http.delete(environment.API_HOST + '/keys',
|
||||
{
|
||||
body: {
|
||||
key: key_id,
|
||||
|
@ -1,6 +1,13 @@
|
||||
@use '@angular/material' as mat;
|
||||
|
||||
ul {
|
||||
background-color: rgb(202, 68, 255);
|
||||
border-radius: 15px;
|
||||
margin: 0 0;
|
||||
padding: 0;
|
||||
max-width: 600px;
|
||||
overflow: hidden;
|
||||
|
||||
@include mat.all-component-densities(-5);
|
||||
|
||||
@include mat.form-field-overrides((
|
||||
@ -8,13 +15,6 @@ ul {
|
||||
outlined-focus-label-text-color: rgb(155, 57, 194),
|
||||
outlined-focus-outline-color: rgb(155, 57, 194),
|
||||
));
|
||||
|
||||
background-color: rgb(202, 68, 255);
|
||||
border-radius: 15px;
|
||||
margin: 0 0;
|
||||
padding: 0;
|
||||
max-width: 600px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
ul li {
|
||||
|
@ -1,5 +1,6 @@
|
||||
@use "@angular/material" as mat;
|
||||
|
||||
|
||||
ul {
|
||||
padding: 0;
|
||||
}
|
||||
@ -8,9 +9,9 @@ li {
|
||||
list-style: none;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
a {
|
||||
|
@ -5,8 +5,8 @@
|
||||
</button>
|
||||
|
||||
<span>Tom-to-Speech</span>
|
||||
@if (isLoggedIn) {
|
||||
<span class="spacer"></span>
|
||||
@if (isLoggedIn) {
|
||||
<div class="links">
|
||||
@if (showImpersonation) {
|
||||
<impersonation />
|
||||
@ -28,4 +28,10 @@
|
||||
</div>
|
||||
}
|
||||
<theme />
|
||||
|
||||
@if (isLoggedIn) {
|
||||
<logoff-button />
|
||||
} @else {
|
||||
<login-button />
|
||||
}
|
||||
</mat-toolbar>
|
@ -6,10 +6,11 @@ import { MatButtonModule } from '@angular/material/button';
|
||||
import { AuthModule } from '../../auth/auth.module';
|
||||
import { ApiAuthenticationService } from '../../shared/services/api/api-authentication.service';
|
||||
import { ImpersonationComponent } from '../../auth/impersonation/impersonation.component';
|
||||
import { HermesClientService } from '../../hermes-client.service';
|
||||
import EventService from '../../shared/services/EventService';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { ThemeComponent } from "../../theme/theme.component";
|
||||
import { LogoffButtonComponent } from "../../auth/logoff-button/logoff-button.component";
|
||||
import { LoginButtonComponent } from "../../auth/login-button/login-button.component";
|
||||
|
||||
@Component({
|
||||
selector: 'topbar',
|
||||
@ -21,6 +22,8 @@ import { ThemeComponent } from "../../theme/theme.component";
|
||||
MatIconModule,
|
||||
MatToolbarModule,
|
||||
ThemeComponent,
|
||||
LoginButtonComponent,
|
||||
LogoffButtonComponent,
|
||||
],
|
||||
providers: [AuthVisitorGuard],
|
||||
templateUrl: './topbar.component.html',
|
||||
@ -28,14 +31,13 @@ import { ThemeComponent } from "../../theme/theme.component";
|
||||
})
|
||||
export class Topbar implements OnDestroy {
|
||||
private readonly auth = inject(ApiAuthenticationService);
|
||||
private readonly client = inject(HermesClientService);
|
||||
private readonly events = inject(EventService);
|
||||
|
||||
private subscriptions: (Subscription | null)[] = [];
|
||||
private _showImpersonation: boolean = false
|
||||
|
||||
constructor() {
|
||||
this.subscriptions.push(this.events.listen('impersonation', () => this.showImpersonation = false));
|
||||
this.subscriptions.push(this.events.listen('impersonation', () => { this.auth.update(localStorage.getItem('jwt')); this.showImpersonation = false; }));
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
@ -51,7 +53,7 @@ export class Topbar implements OnDestroy {
|
||||
}
|
||||
|
||||
get isAdminLoggedIn() {
|
||||
return this.auth.isAuthenticated() && this.auth.isAdmin();
|
||||
return this.auth.isAdmin();
|
||||
}
|
||||
|
||||
get username() {
|
||||
|
@ -58,7 +58,7 @@ export class PermissionItemEditComponent implements OnInit {
|
||||
}
|
||||
|
||||
submit() {
|
||||
if (this.form.invalid || this.waitForResponse) {
|
||||
if (!this.form.dirty || this.form.invalid || this.waitForResponse) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,13 @@
|
||||
@use '@angular/material' as mat;
|
||||
|
||||
ul {
|
||||
background-color: rgb(202, 68, 255);
|
||||
border-radius: 15px;
|
||||
margin: 0 0;
|
||||
padding: 0;
|
||||
max-width: 500px;
|
||||
overflow: hidden;
|
||||
|
||||
@include mat.all-component-densities(-5);
|
||||
|
||||
@include mat.form-field-overrides((
|
||||
@ -8,13 +15,6 @@ ul {
|
||||
outlined-focus-label-text-color: rgb(155, 57, 194),
|
||||
outlined-focus-outline-color: rgb(155, 57, 194),
|
||||
));
|
||||
|
||||
background-color: rgb(202, 68, 255);
|
||||
border-radius: 15px;
|
||||
margin: 0 0;
|
||||
padding: 0;
|
||||
max-width: 500px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
ul li {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, EventEmitter, inject, Input, Output } from '@angular/core';
|
||||
import { Component, inject, input } 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';
|
||||
@ -17,28 +17,21 @@ import { Group } from '../../shared/models/group';
|
||||
})
|
||||
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>();
|
||||
|
||||
policies = input.required<Policy[]>();
|
||||
groups = input.required<Group[]>();
|
||||
group = input<string | undefined>();
|
||||
|
||||
|
||||
openDialog(): void {
|
||||
const dialogRef = this.dialog.open(PolicyItemEditComponent, {
|
||||
this.dialog.open(PolicyItemEditComponent, {
|
||||
data: {
|
||||
policies: this.policies,
|
||||
groups: this.groups,
|
||||
group_id: this.group,
|
||||
groupDisabled: !!this.group,
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
@ -7,8 +7,11 @@
|
||||
[formControl]="policyControl"
|
||||
[matAutocomplete]="auto" />
|
||||
<mat-autocomplete #auto="matAutocomplete">
|
||||
@for (option of filteredPolicies | async; track option) {
|
||||
<mat-option [value]="option">{{option}}</mat-option>
|
||||
@for (option of filteredPolicies | async; track option.path) {
|
||||
<mat-option [value]="option.path">
|
||||
<p class="path">{{option.path}}</p>
|
||||
<p class="description muted">{{option.description}}</p>
|
||||
</mat-option>
|
||||
}
|
||||
</mat-autocomplete>
|
||||
@if (policyControl.invalid && (policyControl.dirty || policyControl.touched)) {
|
||||
|
@ -0,0 +1,12 @@
|
||||
p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: #999999
|
||||
}
|
@ -4,9 +4,14 @@ import { FormControl, FormsModule, ReactiveFormsModule, Validators } from '@angu
|
||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { map, Observable, startWith } from 'rxjs';
|
||||
import { EMPTY, map, Observable, startWith } from 'rxjs';
|
||||
|
||||
const Policies = [
|
||||
interface PolicyData {
|
||||
path: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const Policies: PolicyData[] = [
|
||||
{ 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" },
|
||||
@ -43,14 +48,7 @@ const Policies = [
|
||||
export class PolicyDropdownComponent {
|
||||
@Input() policy: string | null = '';
|
||||
@Input({ alias: 'control' }) policyControl = new FormControl('', [Validators.required]);
|
||||
filteredPolicies: Observable<string[]>;
|
||||
|
||||
constructor() {
|
||||
this.filteredPolicies = this.policyControl.valueChanges.pipe(
|
||||
startWith(''),
|
||||
map(value => this._filter(value || '')),
|
||||
);
|
||||
}
|
||||
filteredPolicies: Observable<PolicyData[]> = EMPTY;
|
||||
|
||||
ngOnInit() {
|
||||
this.policyControl.setValue(this.policy);
|
||||
@ -60,13 +58,12 @@ export class PolicyDropdownComponent {
|
||||
);
|
||||
}
|
||||
|
||||
private _filter(value: string): string[] {
|
||||
private _filter(value: string): PolicyData[] {
|
||||
const filterValue = value.toLowerCase();
|
||||
const names = Policies.map(p => p.path);
|
||||
if (names.includes(filterValue)) {
|
||||
return names;
|
||||
if (Policies.map(p => p.path).includes(filterValue)) {
|
||||
return Policies;
|
||||
}
|
||||
|
||||
return names.filter(option => option.toLowerCase().includes(filterValue));
|
||||
return Policies.filter(option => option.path.toLowerCase().includes(filterValue));
|
||||
}
|
||||
}
|
||||
|
@ -60,6 +60,7 @@
|
||||
</button>
|
||||
<button mat-button
|
||||
class="confirm"
|
||||
[disabled]="!formGroup.dirty || formGroup.invalid || waitForResponse"
|
||||
(click)="save()">
|
||||
<mat-icon>{{(isNew ? 'add' : 'save')}}</mat-icon>{{(isNew ? 'Add' : 'Save')}}
|
||||
</button>
|
||||
|
@ -79,7 +79,7 @@ export class PolicyItemEditComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
|
||||
save() {
|
||||
if (this.formGroup.invalid || this.waitForResponse)
|
||||
if (!this.formGroup.dirty || this.formGroup.invalid || this.waitForResponse)
|
||||
return;
|
||||
|
||||
this.waitForResponse = true;
|
||||
|
@ -1,5 +1,5 @@
|
||||
<table mat-table
|
||||
[dataSource]="policies"
|
||||
[dataSource]="policies()"
|
||||
class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="path">
|
||||
<th mat-header-cell
|
||||
|
@ -1,79 +1,48 @@
|
||||
import { AfterViewInit, Component, inject, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, ViewChild } from '@angular/core';
|
||||
import { Component, inject, input, OnDestroy, OnInit, 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 { 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';
|
||||
import PolicyService from '../../shared/services/policy.service';
|
||||
|
||||
@Component({
|
||||
selector: 'policy-table',
|
||||
imports: [FormsModule, MatButtonModule, MatTableModule, MatIconModule],
|
||||
imports: [
|
||||
FormsModule,
|
||||
MatButtonModule,
|
||||
MatTableModule,
|
||||
MatIconModule,
|
||||
],
|
||||
templateUrl: './policy-table.component.html',
|
||||
styleUrl: './policy-table.component.scss'
|
||||
})
|
||||
export class PolicyTableComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly hermes = inject(HermesClientService);
|
||||
private readonly events = inject(EventService);
|
||||
export class PolicyTableComponent implements OnInit, OnDestroy {
|
||||
private readonly client = inject(HermesClientService);
|
||||
private readonly policyService = inject(PolicyService);
|
||||
private readonly dialog = inject(MatDialog);
|
||||
|
||||
@Input() policies: Policy[] = [];
|
||||
policies = input.required<Policy[]>();
|
||||
groups = input.required<Group[]>();
|
||||
|
||||
@ViewChild(MatTable) table: MatTable<Policy>;
|
||||
|
||||
readonly displayedColumns = ['path', 'group', 'usage', 'span', 'actions'];
|
||||
private readonly _subscriptions: any[] = [];
|
||||
|
||||
groups: Group[] = [];
|
||||
|
||||
|
||||
constructor() {
|
||||
this.table = {} as MatTable<Policy>;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.route.data.subscribe(r => {
|
||||
this.groups = [...r['groups']];
|
||||
});
|
||||
|
||||
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();
|
||||
}));
|
||||
|
||||
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();
|
||||
}));
|
||||
|
||||
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._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();
|
||||
this._subscriptions.push(this.policyService.create$?.subscribe(_ => this.table.renderRows()));
|
||||
this._subscriptions.push(this.policyService.update$?.subscribe(_ => this.table.renderRows()));
|
||||
this._subscriptions.push(this.policyService.delete$?.subscribe(_ => this.table.renderRows()));
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
@ -81,33 +50,23 @@ export class PolicyTableComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
}
|
||||
|
||||
delete(policy: Policy) {
|
||||
this.hermes.deletePolicy(policy.id);
|
||||
this.client.deletePolicy(policy.id);
|
||||
}
|
||||
|
||||
edit(policy: Policy) {
|
||||
const dialogRef = this.dialog.open(PolicyItemEditComponent, {
|
||||
this.dialog.open(PolicyItemEditComponent, {
|
||||
data: {
|
||||
policies: this.policies,
|
||||
groups: this.groups,
|
||||
policies: this.policies(),
|
||||
groups: this.groups(),
|
||||
policy_id: policy.id,
|
||||
group_id: policy.group_id,
|
||||
groupDisabled: true,
|
||||
isNew: false,
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result: Policy) => {
|
||||
if (!result)
|
||||
return;
|
||||
|
||||
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);
|
||||
return this.groups().find((g: Group) => g.id == group_id);
|
||||
}
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
<h4>Policies</h4>
|
||||
<div class="add">
|
||||
<policy-add-button [policies]="policies"
|
||||
[groups]="groups"
|
||||
(policy)="addPolicy($event)" />
|
||||
[groups]="groups" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<policy-table [policies]="policies" />
|
||||
<policy-table [policies]="policies"
|
||||
[groups]="groups" />
|
||||
</div>
|
@ -1,4 +1,4 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { Component, inject, OnDestroy } from '@angular/core';
|
||||
import { PolicyTableComponent } from "../policy-table/policy-table.component";
|
||||
import { Policy } from '../../shared/models/policy';
|
||||
import { ActivatedRoute, RouterModule } from '@angular/router';
|
||||
@ -6,43 +6,55 @@ 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';
|
||||
import PolicyService from '../../shared/services/policy.service';
|
||||
import GroupService from '../../shared/services/group.service';
|
||||
|
||||
@Component({
|
||||
selector: 'policy',
|
||||
imports: [MatButtonModule, MatIconModule, PolicyTableComponent, RouterModule, PolicyAddButtonComponent],
|
||||
imports: [
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
PolicyTableComponent,
|
||||
RouterModule,
|
||||
PolicyAddButtonComponent,
|
||||
],
|
||||
templateUrl: './policy.component.html',
|
||||
styleUrl: './policy.component.scss'
|
||||
})
|
||||
export class PolicyComponent {
|
||||
export class PolicyComponent implements OnDestroy {
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
|
||||
private readonly policyService = inject(PolicyService);
|
||||
private readonly groupService = inject(GroupService);
|
||||
|
||||
private readonly _subscriptions: any[] = [];
|
||||
private _policies: Policy[] = [];
|
||||
groups: Group[] = [];
|
||||
private _groups: Group[] = [];
|
||||
|
||||
|
||||
constructor() {
|
||||
this.route.data.subscribe((data) => {
|
||||
const policies = [...data['policies']];
|
||||
policies.sort(this.compare);
|
||||
this._policies = policies;
|
||||
|
||||
this.groups = [...data['groups']];
|
||||
this._policies = data['policies'];
|
||||
this._groups = data['groups'];
|
||||
});
|
||||
|
||||
this._subscriptions.push(this.policyService.delete$?.subscribe(_ =>
|
||||
this.policyService.fetch().subscribe(p => this._policies = p)));
|
||||
|
||||
this._subscriptions.push(this.groupService.deleteGroup$?.subscribe(_ =>
|
||||
this.groupService.fetch().subscribe(g => this._groups = g.groups)));
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this._subscriptions.filter(s => !!s).forEach(s => s.unsubscribe());
|
||||
}
|
||||
|
||||
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 groups() {
|
||||
return this._groups;
|
||||
}
|
||||
|
||||
compare(a: Policy, b: Policy) {
|
||||
|
@ -1,4 +1,9 @@
|
||||
|
||||
.buttons {
|
||||
margin: -0.75em;
|
||||
}
|
||||
|
||||
.mat-mdc-card-content {
|
||||
display: grid;
|
||||
row-gap: 1em;
|
||||
align-self: center;
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { Component, inject, Input, model, OnInit, signal } from '@angular/core';
|
||||
import { Component, inject, OnInit } from '@angular/core';
|
||||
import Redemption from '../../shared/models/redemption';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
@ -14,8 +14,6 @@ import TwitchRedemption from '../../shared/models/twitch-redemption';
|
||||
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 { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'redemption-item-edit',
|
||||
@ -33,10 +31,9 @@ import { Subscription } from 'rxjs';
|
||||
styleUrl: './redemption-item-edit.component.scss'
|
||||
})
|
||||
export class RedemptionItemEditComponent implements OnInit {
|
||||
readonly client = inject(HermesClientService);
|
||||
readonly redemptionService = inject(RedemptionService);
|
||||
readonly dialogRef = inject(MatDialogRef<RedemptionItemEditComponent>);
|
||||
readonly data = inject<{ redemption: Redemption, twitchRedemptions: TwitchRedemption[], redeemableActions: RedeemableAction[] }>(MAT_DIALOG_DATA);
|
||||
private readonly client = inject(HermesClientService);
|
||||
private readonly dialogRef = inject(MatDialogRef<RedemptionItemEditComponent>);
|
||||
private readonly data = inject<{ redemption: Redemption, twitchRedemptions: { [id: string]: TwitchRedemption }, redeemableActions: RedeemableAction[] }>(MAT_DIALOG_DATA);
|
||||
|
||||
redemptionFormControl = new FormControl<TwitchRedemption | string | undefined>(undefined, [Validators.required, createTypeValidator('Object')]);
|
||||
redemptionErrorMessages: { [errorKey: string]: string } = {
|
||||
@ -66,16 +63,17 @@ export class RedemptionItemEditComponent implements OnInit {
|
||||
});
|
||||
|
||||
redemption: Redemption = { id: '', user_id: '', redemption_id: '', action_name: '', order: 0, state: true };
|
||||
twitchRedemptions: TwitchRedemption[] = [];
|
||||
twitchRedemptions: { [id: string]: TwitchRedemption } = {};
|
||||
redeemableActions: RedeemableAction[] = [];
|
||||
|
||||
waitForResponse = false;
|
||||
responseError: string | undefined = undefined;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.redemption = this.data.redemption;
|
||||
this.orderFormControl.setValue(this.redemption.order);
|
||||
this.twitchRedemptions = this.data.twitchRedemptions;
|
||||
this.redeemableActions = this.data.redeemableActions;
|
||||
this.orderFormControl.setValue(this.redemption.order);
|
||||
this.orderErrorMessageKeys = Object.keys(this.orderErrorMessages);
|
||||
}
|
||||
|
||||
@ -84,7 +82,9 @@ export class RedemptionItemEditComponent implements OnInit {
|
||||
if (this.waitForResponse)
|
||||
return;
|
||||
|
||||
this.waitForResponse = true
|
||||
this.waitForResponse = true;
|
||||
this.responseError = undefined;
|
||||
|
||||
const id = this.redemption.id;
|
||||
this.client.first((d: any) => d.op == 4 && d.d.request.type == 'delete_redemption' && d.d.request.data.id == id)
|
||||
?.subscribe({
|
||||
@ -102,11 +102,12 @@ export class RedemptionItemEditComponent implements OnInit {
|
||||
}
|
||||
|
||||
save() {
|
||||
if (this.formGroups.invalid || this.waitForResponse)
|
||||
if (!this.formGroups.dirty || this.formGroups.invalid || this.waitForResponse)
|
||||
return;
|
||||
|
||||
this.waitForResponse = true;
|
||||
this.responseError = undefined;
|
||||
|
||||
const order = this.orderFormControl.value;
|
||||
if (order == null) {
|
||||
this.responseError = 'Order must be an integer.';
|
||||
@ -114,16 +115,14 @@ export class RedemptionItemEditComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
this.waitForResponse = true;
|
||||
const isNew = !this.redemption.id;
|
||||
if (isNew) {
|
||||
this.client.first((d: any) => d.op == 4 && d.d.request.type == 'create_redemption' && d.d.request.data.action == (this.redemption.action_name ?? '') && d.d.request.data.redemption == (this.redemption.redemption_id ?? ''))
|
||||
this.client.first((d: any) => d.op == 4 && d.d.request.type == 'create_redemption' && d.d.request.data.action == this.redemption.action_name && d.d.request.data.redemption == this.redemption.redemption_id)
|
||||
?.subscribe({
|
||||
next: (d) => {
|
||||
if (d.d.error) {
|
||||
this.responseError = d.d.error;
|
||||
} else {
|
||||
this.redemption.order = order;
|
||||
this.dialogRef.close(d.d.data);
|
||||
}
|
||||
},
|
||||
@ -132,13 +131,13 @@ export class RedemptionItemEditComponent implements OnInit {
|
||||
});
|
||||
this.client.createRedemption(this.redemption.redemption_id, this.redemption.action_name, order);
|
||||
} else {
|
||||
|
||||
this.client.first((d: any) => d.op == 4 && d.d.request.type == 'update_redemption' && d.d.data.id == this.redemption.id)
|
||||
?.subscribe({
|
||||
next: (d) => {
|
||||
if (d.d.error) {
|
||||
this.responseError = d.d.error;
|
||||
} else {
|
||||
this.redemption.order = order;
|
||||
this.dialogRef.close(d.d.data);
|
||||
}
|
||||
},
|
||||
|
@ -1,4 +1,4 @@
|
||||
<div class="content">
|
||||
<content>
|
||||
<button mat-button
|
||||
class="add"
|
||||
(click)="add()"><mat-icon>add</mat-icon> Add Redemption</button>
|
||||
@ -14,10 +14,12 @@
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
<div class="filters">
|
||||
<twitch-redemption-dropdown [(twitchRedemptionId)]="filter_redemption"
|
||||
[search]="true" />
|
||||
<action-dropdown [(action)]="filter_action_name"
|
||||
<twitch-redemption-dropdown [twitchRedemptions]="twitchRedemptions()"
|
||||
[(twitchRedemptionId)]="filter_redemption"
|
||||
[search]="true" />
|
||||
<action-dropdown [search]="true"
|
||||
[actions]="actions()"
|
||||
[(action)]="filter_action_name" />
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
|
||||
@ -64,4 +66,4 @@
|
||||
*matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</content>
|
@ -1,9 +1,3 @@
|
||||
.content {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.add {
|
||||
width: 100%;
|
||||
margin-top: 3em;
|
||||
@ -21,10 +15,10 @@
|
||||
}
|
||||
|
||||
.table-container {
|
||||
min-width: 800px;
|
||||
width: 800px;
|
||||
flex: 1;
|
||||
height: 60vh;
|
||||
overflow: auto;
|
||||
overflow: scroll;
|
||||
margin-bottom: 2em;
|
||||
border-radius: 15px;
|
||||
}
|
@ -1,23 +1,19 @@
|
||||
import { Component, inject, OnDestroy, signal } from '@angular/core';
|
||||
import RedemptionService from '../../shared/services/redemption.service';
|
||||
import { Component, inject, input, signal } from '@angular/core';
|
||||
import Redemption from '../../shared/models/redemption';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { TwitchRedemptionDropdownComponent } from "../twitch-redemption-dropdown/twitch-redemption-dropdown.component";
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { ActionDropdownComponent } from '../../actions/action-dropdown/action-dropdown.component';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
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 { MatExpansionModule } from '@angular/material/expansion';
|
||||
import { ScrollingModule } from '@angular/cdk/scrolling';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { HermesClientService } from '../../hermes-client.service';
|
||||
import { toTwitchRedemptionDict } from '../../shared/transformers/twitch-redemption.transformer';
|
||||
|
||||
@Component({
|
||||
selector: 'redemption-list',
|
||||
@ -36,97 +32,44 @@ import { HermesClientService } from '../../hermes-client.service';
|
||||
templateUrl: './redemption-list.component.html',
|
||||
styleUrl: './redemption-list.component.scss'
|
||||
})
|
||||
export class RedemptionListComponent implements OnDestroy {
|
||||
private readonly client = inject(HermesClientService);
|
||||
private readonly redemptionService = inject(RedemptionService);
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
export class RedemptionListComponent {
|
||||
readonly dialog = inject(MatDialog);
|
||||
private _redemptions: Redemption[] = [];
|
||||
private _twitchRedemptions: TwitchRedemption[] = [];
|
||||
private _twitchRedemptionsDict: { [id: string]: string } = {};
|
||||
private _actions: RedeemableAction[] = [];
|
||||
displayedColumns: string[] = ['twitch-redemption', 'action-name', 'order', 'misc'];
|
||||
filter_redemption: string | undefined;
|
||||
filter_action_name: string | undefined;
|
||||
readonly panelOpenState = signal(false);
|
||||
private _subscriptions: Subscription[] = []
|
||||
|
||||
|
||||
constructor() {
|
||||
this.route.data.subscribe(r => {
|
||||
this._twitchRedemptions = r['twitchRedemptions'];
|
||||
this._twitchRedemptionsDict = Object.assign({}, ...r['twitchRedemptions'].map((t: TwitchRedemption) => ({ [t.id]: t.title })));
|
||||
this._actions = r['redeemableActions'];
|
||||
|
||||
let redemptions = [...r['redemptions']];
|
||||
redemptions.sort((a: Redemption, b: Redemption) => this.compare(a, b));
|
||||
this._redemptions = redemptions;
|
||||
_redemptions = input.required<Redemption[]>({ alias: 'redemptions' });
|
||||
twitchRedemptions = input.required({
|
||||
alias: 'twitchRedemptions',
|
||||
transform: toTwitchRedemptionDict,
|
||||
});
|
||||
actions = input.required<RedeemableAction[]>();
|
||||
|
||||
let subscription = this.redemptionService.create$?.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._redemptions.length; i++) {
|
||||
const comp = this.compare(d.data, this._redemptions[i]);
|
||||
if (comp < 0) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
this._redemptions.splice(index >= 0 ? index : this._redemptions.length, 0, d.data);
|
||||
});
|
||||
if (subscription)
|
||||
this._subscriptions.push(subscription);
|
||||
|
||||
subscription = this.redemptionService.update$?.subscribe(d => {
|
||||
if (d.error || !d.data || d.request.nounce != null && d.request.nounce.startsWith(this.client.session_id))
|
||||
return;
|
||||
|
||||
const redemption = this._redemptions.find(r => r.id = d.data.id);
|
||||
if (redemption) {
|
||||
redemption.action_name = d.data.action_name;
|
||||
redemption.redemption_id = d.data.redemption_id;
|
||||
redemption.order = d.data.order;
|
||||
redemption.state = d.data.state;
|
||||
}
|
||||
});
|
||||
if (subscription)
|
||||
this._subscriptions.push(subscription);
|
||||
|
||||
subscription = this.redemptionService.delete$?.subscribe(d => {
|
||||
if (d.error || d.request.nounce != null && d.request.nounce.startsWith(this.client.session_id))
|
||||
return;
|
||||
|
||||
this._redemptions = this._redemptions.filter(r => r.id != d.request.data.id);
|
||||
});
|
||||
if (subscription)
|
||||
this._subscriptions.push(subscription);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.panelOpenState.set(false);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this._subscriptions.forEach(s => s.unsubscribe());
|
||||
}
|
||||
|
||||
compare(a: Redemption, b: Redemption) {
|
||||
return this._twitchRedemptionsDict[a.redemption_id].localeCompare(this._twitchRedemptionsDict[b.redemption_id]) || a.order - b.order;
|
||||
const dict = this.twitchRedemptions();
|
||||
if (!dict) {
|
||||
return 0;
|
||||
}
|
||||
return dict[a.redemption_id]?.title.localeCompare(dict[b.redemption_id]?.title) || a.order - b.order;
|
||||
}
|
||||
|
||||
get redemptions() {
|
||||
const redemptionFilter = this.filter_redemption?.toString().toLowerCase();
|
||||
const actionFilter = this.filter_action_name?.toString().toLowerCase();
|
||||
let filtered = this._redemptions.filter(r => !redemptionFilter || this._twitchRedemptionsDict[r.redemption_id].toLowerCase().includes(redemptionFilter));
|
||||
const redemptionFilter = this.filter_redemption?.toString().toLowerCase();
|
||||
let filtered = this._redemptions();
|
||||
if (redemptionFilter) {
|
||||
filtered = filtered.filter(r => this.twitchRedemptions()![r.redemption_id]?.title.toLowerCase().includes(redemptionFilter) || r.redemption_id == redemptionFilter);
|
||||
}
|
||||
if (actionFilter) {
|
||||
filtered = filtered.filter(r => !actionFilter || r.action_name.toLowerCase().includes(actionFilter));
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
getTwitchRedemptionNameById(id: string) {
|
||||
return this._twitchRedemptionsDict[id];
|
||||
return this.twitchRedemptions()![id]?.title;
|
||||
}
|
||||
|
||||
add(): void {
|
||||
@ -134,39 +77,9 @@ export class RedemptionListComponent implements OnDestroy {
|
||||
}
|
||||
|
||||
openDialog(redemption: Redemption): void {
|
||||
const dialogRef = this.dialog.open(RedemptionItemEditComponent, {
|
||||
data: { redemption: { ...redemption }, twitchRedemptions: this._twitchRedemptions, redeemableActions: this._actions },
|
||||
this.dialog.open(RedemptionItemEditComponent, {
|
||||
data: { redemption: { ...redemption }, twitchRedemptions: this.twitchRedemptions(), redeemableActions: this.actions() },
|
||||
maxWidth: '100vw'
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result: Redemption | string) => {
|
||||
if (!result)
|
||||
return;
|
||||
|
||||
if (typeof result === 'string') {
|
||||
// Deleted
|
||||
this._redemptions = this._redemptions.filter(r => r.id != result);
|
||||
} else {
|
||||
const redemption = this._redemptions.find(r => r.id == result.id);
|
||||
if (redemption) {
|
||||
// Updated
|
||||
redemption.action_name = result.action_name;
|
||||
redemption.redemption_id = result.redemption_id;
|
||||
redemption.order = result.order;
|
||||
redemption.state = result.state;
|
||||
} else {
|
||||
// Created
|
||||
let index = -1;
|
||||
for (let i = 0; i < this._redemptions.length; i++) {
|
||||
const comp = this.compare(result, this._redemptions[i]);
|
||||
if (comp < 0) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
this._redemptions.splice(index >= 0 ? index : this._redemptions.length, 0, result);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
<div class="root">
|
||||
<div class="content">
|
||||
<redemption-list />
|
||||
<redemption-list [redemptions]="redemptions$ | async"
|
||||
[twitchRedemptions]="twitchRedemptions$ | async"
|
||||
[actions]="actions$ | async" />
|
||||
</div>
|
||||
</div>
|
@ -1,26 +1,25 @@
|
||||
import { Component, inject, OnInit } from '@angular/core';
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { RedemptionListComponent } from "../redemption-list/redemption-list.component";
|
||||
import { HermesClientService } from '../../hermes-client.service';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import RedemptionService from '../../shared/services/redemption.service';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import Redemption from '../../shared/models/redemption';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import TwitchRedemptionService from '../../shared/services/twitch-redemption.service';
|
||||
import RedeemableActionService from '../../shared/services/redeemable-action.service';
|
||||
|
||||
@Component({
|
||||
selector: 'redemptions',
|
||||
imports: [RedemptionListComponent],
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
RedemptionListComponent
|
||||
],
|
||||
templateUrl: './redemptions.component.html',
|
||||
styleUrl: './redemptions.component.scss'
|
||||
})
|
||||
export class RedemptionsComponent implements OnInit {
|
||||
client = inject(HermesClientService);
|
||||
http = inject(HttpClient);
|
||||
route = inject(ActivatedRoute);
|
||||
redemptionService = inject(RedemptionService);
|
||||
redemptions: Observable<Redemption[]> | undefined;
|
||||
|
||||
ngOnInit(): void {
|
||||
export class RedemptionsComponent {
|
||||
private readonly twitchRedemptionService = inject(TwitchRedemptionService);
|
||||
private readonly redemptionService = inject(RedemptionService);
|
||||
private readonly actionService = inject(RedeemableActionService);
|
||||
|
||||
}
|
||||
redemptions$ = this.redemptionService.changes$;
|
||||
twitchRedemptions$ = this.twitchRedemptionService.fetch();
|
||||
actions$ = this.actionService.changes$;
|
||||
}
|
||||
|
@ -11,7 +11,7 @@
|
||||
<mat-autocomplete #auto="matAutocomplete"
|
||||
[displayWith]="displayFn"
|
||||
(optionSelected)="select($event.option.value)">
|
||||
@for (redemption of filteredRedemptions; track redemption.id) {
|
||||
@for (redemption of filteredRedemptions; track redemption.title) {
|
||||
<mat-option [value]="redemption">{{redemption.title}}</mat-option>
|
||||
}
|
||||
</mat-autocomplete>
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { Component, EventEmitter, inject, input, Input, OnInit, Output } from '@angular/core';
|
||||
import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { Component, input, Input, model, OnInit } from '@angular/core';
|
||||
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import TwitchRedemption from '../../shared/models/twitch-redemption';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { toTwitchRedemptionArray } from '../../shared/transformers/twitch-redemption.transformer';
|
||||
|
||||
@Component({
|
||||
selector: 'twitch-redemption-dropdown',
|
||||
@ -16,69 +16,60 @@ export class TwitchRedemptionDropdownComponent implements OnInit {
|
||||
@Input() formControl = new FormControl<TwitchRedemption | string | undefined>(undefined);
|
||||
@Input() errorMessages: { [errorKey: string]: string } = {};
|
||||
|
||||
@Input() search: boolean = false;
|
||||
@Input() twitchRedemptions: TwitchRedemption[] = [];
|
||||
@Input() twitchRedemptionId: string | undefined;
|
||||
@Output() readonly twitchRedemptionIdChange = new EventEmitter<string>();
|
||||
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
twitchRedemptions = input.required({
|
||||
transform: toTwitchRedemptionArray
|
||||
});
|
||||
twitchRedemptionId = model<string | undefined>();
|
||||
search = input<boolean>(false);
|
||||
|
||||
errorMessageKeys: string[] = [];
|
||||
|
||||
constructor() {
|
||||
this.route.data.subscribe(data => {
|
||||
if (!data['twitchRedemptions'])
|
||||
return;
|
||||
|
||||
this.twitchRedemptions = data['twitchRedemptions'];
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.errorMessageKeys = Object.keys(this.errorMessages);
|
||||
|
||||
if (!this.twitchRedemptionId || !this.twitchRedemptions)
|
||||
return;
|
||||
|
||||
const redemption = this.twitchRedemptions.find(r => r.id == this.twitchRedemptionId);
|
||||
if (this.twitchRedemptions() && this.twitchRedemptionId()) {
|
||||
const redemption = this.twitchRedemptions()!.find(r => r.id == this.twitchRedemptionId());
|
||||
this.formControl.setValue(redemption);
|
||||
}
|
||||
this.errorMessageKeys = Object.keys(this.errorMessages);
|
||||
}
|
||||
|
||||
get filteredRedemptions() {
|
||||
const value = this.formControl.value;
|
||||
if (typeof (value) == 'string') {
|
||||
return this.twitchRedemptions.filter(r => r.title.toLowerCase().includes(value.toLowerCase()));
|
||||
if (this.twitchRedemptions() && typeof (value) == 'string') {
|
||||
return this.twitchRedemptions()!.filter(r => r.title.toLowerCase().includes(value.toLowerCase()));
|
||||
}
|
||||
return this.twitchRedemptions;
|
||||
return this.twitchRedemptions();
|
||||
}
|
||||
|
||||
select(event: TwitchRedemption) {
|
||||
this.twitchRedemptionIdChange.emit(event.id);
|
||||
this.twitchRedemptionId.set(event.id);
|
||||
}
|
||||
|
||||
input() {
|
||||
if (this.search && typeof this.formControl.value == 'string') {
|
||||
this.twitchRedemptionIdChange.emit(this.formControl.value);
|
||||
if (this.search() && typeof this.formControl.value == 'string') {
|
||||
this.twitchRedemptionId.set(this.formControl.value);
|
||||
}
|
||||
}
|
||||
|
||||
blur() {
|
||||
if (this.filteredRedemptions == null)
|
||||
return;
|
||||
|
||||
if (!this.search && typeof this.formControl.value == 'string') {
|
||||
const name = this.formControl.value;
|
||||
const nameLower = name.toLowerCase();
|
||||
let newValue: TwitchRedemption | undefined = undefined;
|
||||
const insenstiveActions = this.filteredRedemptions.filter(a => a.title.toLowerCase() == nameLower);
|
||||
if (insenstiveActions.length > 1) {
|
||||
const sensitiveAction = insenstiveActions.find(a => a.title == name);
|
||||
newValue = sensitiveAction ?? undefined;
|
||||
} else if (insenstiveActions.length == 1) {
|
||||
newValue = insenstiveActions[0];
|
||||
let newValue: TwitchRedemption | undefined;
|
||||
const filtered = this.filteredRedemptions?.filter(a => a.title.toLowerCase() == nameLower);
|
||||
if (filtered.length > 1) {
|
||||
newValue = filtered.find(a => a.title == name);
|
||||
} else if (filtered.length == 1) {
|
||||
newValue = filtered[0];
|
||||
}
|
||||
if (newValue && newValue.id != this.formControl.value) {
|
||||
this.formControl.setValue(newValue);
|
||||
this.twitchRedemptionIdChange.emit(newValue.id);
|
||||
this.twitchRedemptionId.set(newValue.id);
|
||||
} else if (!newValue)
|
||||
this.twitchRedemptionIdChange.emit(undefined);
|
||||
this.twitchRedemptionId.set(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,5 +2,6 @@ export default interface RedeemableAction {
|
||||
user_id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
has_message: boolean;
|
||||
data: any;
|
||||
}
|
13
src/app/shared/pipes/exclude-by-id.pipe.ts
Normal file
13
src/app/shared/pipes/exclude-by-id.pipe.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import {Pipe, PipeTransform} from '@angular/core';
|
||||
|
||||
@Pipe({
|
||||
name: 'excludeById',
|
||||
})
|
||||
export class ExcludeByIdPipe implements PipeTransform {
|
||||
transform(values: any[] | null, id: string): any[] | null {
|
||||
if (!values) {
|
||||
return values;
|
||||
}
|
||||
return values.filter(value => value.id != id);
|
||||
}
|
||||
}
|
@ -1,21 +1,23 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import EventService from '../EventService';
|
||||
import { environment } from '../../../../environments/environment';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ApiAuthenticationService {
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly events = inject(EventService);
|
||||
|
||||
private authenticated: boolean;
|
||||
private user: any;
|
||||
private lastCheck: Date;
|
||||
|
||||
constructor(private http: HttpClient, private events: EventService) {
|
||||
constructor() {
|
||||
this.authenticated = false;
|
||||
this.user = null;
|
||||
this.lastCheck = new Date();
|
||||
|
||||
this.events.listen('impersonation', _ => this.update());
|
||||
}
|
||||
|
||||
isAuthenticated() {
|
||||
@ -34,38 +36,47 @@ export class ApiAuthenticationService {
|
||||
return this.user?.impersonation?.name;
|
||||
}
|
||||
|
||||
getUserId() {
|
||||
return this.user?.id;
|
||||
}
|
||||
|
||||
getUsername() {
|
||||
return this.user?.name;
|
||||
}
|
||||
|
||||
logout() {
|
||||
localStorage.removeItem('jwt');
|
||||
this.updateAuthenticated(false, null);
|
||||
this.updateAuthenticated(null, false, null);
|
||||
}
|
||||
|
||||
update() {
|
||||
const jwt = localStorage.getItem('jwt');
|
||||
update(jwt: string | null) {
|
||||
if (!jwt) {
|
||||
this.updateAuthenticated(false, null);
|
||||
this.updateAuthenticated(null, false, null);
|
||||
return;
|
||||
}
|
||||
|
||||
// /api/auth/validate
|
||||
this.http.get('/api/auth/validate', {
|
||||
this.http.get(environment.API_HOST + '/auth/validate', {
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + jwt
|
||||
}
|
||||
}).subscribe((data: any) => {
|
||||
this.updateAuthenticated(data?.authenticated, data?.user);
|
||||
},
|
||||
withCredentials: true
|
||||
}).subscribe({
|
||||
next: (data: any) => this.updateAuthenticated(jwt, data?.authenticated, data?.user),
|
||||
error: () => this.updateAuthenticated(jwt, false, null)
|
||||
});
|
||||
}
|
||||
|
||||
private updateAuthenticated(authenticated: boolean, user: any) {
|
||||
private updateAuthenticated(jwt: string | null, authenticated: boolean, user: any) {
|
||||
const previous = this.authenticated;
|
||||
this.authenticated = authenticated;
|
||||
this.user = user;
|
||||
this.authenticated = authenticated;
|
||||
this.lastCheck = new Date();
|
||||
|
||||
if (jwt) {
|
||||
localStorage.setItem('jwt', jwt);
|
||||
} else {
|
||||
localStorage.removeItem('jwt');
|
||||
}
|
||||
|
||||
if (previous != authenticated) {
|
||||
if (authenticated) {
|
||||
this.events.emit('login', null);
|
||||
|
@ -16,11 +16,9 @@ export class ApiKeyService {
|
||||
|
||||
|
||||
constructor() {
|
||||
this.events.listen('tts_logoff', (impersonation) => {
|
||||
if (impersonation) {
|
||||
this.events.listen('impersonation', () => {
|
||||
this.keys = [];
|
||||
this.loaded = false;
|
||||
}
|
||||
});
|
||||
|
||||
this.events.listen('logoff', () => {
|
||||
|
@ -2,7 +2,7 @@ import { inject, Injectable } from '@angular/core';
|
||||
import { HermesClientService } from '../../hermes-client.service';
|
||||
import EventService from './EventService';
|
||||
import { Permission } from '../models/permission';
|
||||
import { map, Observable, of } from 'rxjs';
|
||||
import { filter, map, merge, Observable, of, startWith } from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@ -12,6 +12,7 @@ export class PermissionService {
|
||||
private readonly events = inject(EventService);
|
||||
private data: Permission[] = [];
|
||||
private loaded = false;
|
||||
changes$: Observable<any>;
|
||||
create$: Observable<any> | undefined;
|
||||
update$: Observable<any> | undefined;
|
||||
delete$: Observable<any> | undefined;
|
||||
@ -20,6 +21,12 @@ export class PermissionService {
|
||||
this.create$ = this.client.filterByRequestType('create_group_permission');
|
||||
this.update$ = this.client.filterByRequestType('update_group_permission');
|
||||
this.delete$ = this.client.filterByRequestType('delete_group_permission');
|
||||
this.changes$ = merge<any>(this.create$, this.update$, this.delete$)
|
||||
.pipe(
|
||||
startWith(null),
|
||||
filter(d => !d.error),
|
||||
map(_ => this.data.slice()),
|
||||
);
|
||||
|
||||
this.create$?.subscribe(d => {
|
||||
if (d.error) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { HermesClientService } from '../../hermes-client.service';
|
||||
import { map, Observable, of } from 'rxjs';
|
||||
import { map, merge, Observable, of, startWith } from 'rxjs';
|
||||
import RedeemableAction from '../models/redeemable-action';
|
||||
import EventService from './EventService';
|
||||
|
||||
@ -10,8 +10,9 @@ import EventService from './EventService';
|
||||
export default class RedeemableActionService {
|
||||
private readonly client = inject(HermesClientService);
|
||||
private readonly events = inject(EventService);
|
||||
private data: RedeemableAction[] = []
|
||||
private data: RedeemableAction[] = [];
|
||||
private loaded = false;
|
||||
changes$: Observable<any>;
|
||||
create$: Observable<any> | undefined;
|
||||
update$: Observable<any> | undefined;
|
||||
delete$: Observable<any> | undefined;
|
||||
@ -21,6 +22,11 @@ export default class RedeemableActionService {
|
||||
this.create$ = this.client.filterByRequestType('create_redeemable_action');
|
||||
this.update$ = this.client.filterByRequestType('update_redeemable_action');
|
||||
this.delete$ = this.client.filterByRequestType('delete_redeemable_action');
|
||||
this.changes$ = merge<any>(this.create$, this.update$, this.delete$)
|
||||
.pipe(
|
||||
startWith(null),
|
||||
map(_ => this.data.slice()),
|
||||
);
|
||||
|
||||
this.create$?.subscribe(d => this.data.push(d.data));
|
||||
this.update$?.subscribe(d => {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import Redemption from '../models/redemption';
|
||||
import { HermesClientService } from '../../hermes-client.service';
|
||||
import { map, Observable, of } from 'rxjs';
|
||||
import { map, merge, Observable, of, startWith } from 'rxjs';
|
||||
import EventService from './EventService';
|
||||
|
||||
@Injectable({
|
||||
@ -12,6 +12,7 @@ export default class RedemptionService {
|
||||
private readonly events = inject(EventService);
|
||||
private data: Redemption[] = []
|
||||
private loaded = false;
|
||||
changes$: Observable<any>;
|
||||
create$: Observable<any> | undefined;
|
||||
update$: Observable<any> | undefined;
|
||||
delete$: Observable<any> | undefined;
|
||||
@ -20,6 +21,11 @@ export default class RedemptionService {
|
||||
this.create$ = this.client.filterByRequestType('create_redemption');
|
||||
this.update$ = this.client.filterByRequestType('update_redemption');
|
||||
this.delete$ = this.client.filterByRequestType('delete_redemption');
|
||||
this.changes$ = merge<any>(this.create$, this.update$, this.delete$)
|
||||
.pipe(
|
||||
startWith(null),
|
||||
map(_ => this.data.slice()),
|
||||
);
|
||||
|
||||
this.create$?.subscribe(d => this.data.push(d.data));
|
||||
this.update$?.subscribe(d => {
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { Injectable, signal } from '@angular/core';
|
||||
import { inject, Injectable, signal } from '@angular/core';
|
||||
import EventService from './EventService';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ThemeService {
|
||||
private events = inject(EventService);
|
||||
private _current = signal<'light' | 'dark'>('dark');
|
||||
|
||||
get theme() {
|
||||
@ -11,7 +13,17 @@ export class ThemeService {
|
||||
}
|
||||
|
||||
set theme(value: 'light' | 'dark') {
|
||||
const previous = this._current();
|
||||
if (previous == value) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._current.set(value);
|
||||
localStorage.setItem('ui-theme', value);
|
||||
this.events.emit('theme_change', {
|
||||
previous_theme: previous,
|
||||
current_theme: value,
|
||||
});
|
||||
}
|
||||
|
||||
isDarkTheme() {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { HermesClientService } from '../../hermes-client.service';
|
||||
import { first, map, Observable, of } from 'rxjs';
|
||||
import { map, Observable, of } from 'rxjs';
|
||||
import EventService from './EventService';
|
||||
import { Filter } from '../models/filter';
|
||||
|
||||
@ -25,9 +25,9 @@ export default class TtsFilterService {
|
||||
this.update$?.subscribe(d => {
|
||||
const filter = this.data.find(r => r.id == d.data.id);
|
||||
if (filter) {
|
||||
filter.search = d.data.action_name;
|
||||
filter.replace = d.data.redemption_id;
|
||||
filter.flag = d.data.order;
|
||||
filter.search = d.data.search;
|
||||
filter.replace = d.data.replace;
|
||||
filter.flag = d.data.flag;
|
||||
}
|
||||
});
|
||||
this.delete$?.subscribe(d => this.data = this.data.filter(r => r.id != d.request.data.id));
|
||||
|
@ -2,7 +2,7 @@ import { HttpClient } from '@angular/common/http';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import TwitchRedemption from '../models/twitch-redemption';
|
||||
import { catchError, EMPTY, Observable, of } from 'rxjs';
|
||||
import { catchError, EMPTY, of } from 'rxjs';
|
||||
import EventService from './EventService';
|
||||
|
||||
@Injectable({
|
||||
@ -11,6 +11,7 @@ import EventService from './EventService';
|
||||
export default class TwitchRedemptionService {
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly events = inject(EventService);
|
||||
|
||||
private twitchRedemptions: TwitchRedemption[] = [];
|
||||
private loaded = false;
|
||||
|
||||
|
18
src/app/shared/transformers/twitch-redemption.transformer.ts
Normal file
18
src/app/shared/transformers/twitch-redemption.transformer.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import TwitchRedemption from "../models/twitch-redemption";
|
||||
import { toDictStringKeyed } from "../utils/array.transformer";
|
||||
|
||||
export function toTwitchRedemptionDict(values: TwitchRedemption[] | null): { [key: string]: TwitchRedemption } {
|
||||
if (!values) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return toDictStringKeyed(values, r => r.id, r => r);
|
||||
}
|
||||
|
||||
export function toTwitchRedemptionArray(values: { [id: string]: TwitchRedemption } | null): TwitchRedemption[] {
|
||||
if (!values) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.values(values);
|
||||
}
|
7
src/app/shared/utils/array.transformer.ts
Normal file
7
src/app/shared/utils/array.transformer.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export function toDictStringKeyed<T, V>(values: T[], keyGetter: { (k: T): string }, valueGetter: { (v: T): V }): { [key: string]: V } {
|
||||
return Object.assign({}, ...values.map((t: T) => ({ [keyGetter(t)]: valueGetter(t) })))
|
||||
}
|
||||
|
||||
export function toDictNumberKeyed<T, V>(values: T[], keyGetter: { (k: T): number }, valueGetter: { (v: T): V }): { [key: number]: V } {
|
||||
return Object.assign({}, ...values.map((t: T) => ({ [keyGetter(t)]: valueGetter(t) })))
|
||||
}
|
@ -54,12 +54,12 @@
|
||||
<mat-card-actions>
|
||||
<button mat-button
|
||||
class="neutral"
|
||||
(click)="onCancelClick()"
|
||||
(click)="cancel()"
|
||||
[disabled]="waitForResponse">
|
||||
<mat-icon>cancel</mat-icon> Cancel</button>
|
||||
<button mat-button
|
||||
class="confirm"
|
||||
(click)="onSaveClick()"
|
||||
(click)="save()"
|
||||
[disabled]="!forms.dirty || forms.invalid || waitForResponse">
|
||||
<mat-icon>save</mat-icon>Save</button>
|
||||
</mat-card-actions>
|
||||
|
@ -29,9 +29,8 @@ import { MatIconModule } from '@angular/material/icon';
|
||||
})
|
||||
export class FilterItemEditComponent {
|
||||
private readonly client = inject(HermesClientService);
|
||||
|
||||
private readonly dialogRef = inject(MatDialogRef<FilterItemEditComponent>);
|
||||
readonly data = inject<Filter>(MAT_DIALOG_DATA);
|
||||
private readonly data = inject<Filter>(MAT_DIALOG_DATA);
|
||||
|
||||
readonly regexOptions = [
|
||||
{
|
||||
@ -52,8 +51,8 @@ export class FilterItemEditComponent {
|
||||
},
|
||||
];
|
||||
|
||||
readonly searchControl = new FormControl(this.data.search, [Validators.required]);
|
||||
readonly replaceControl = new FormControl(this.data.replace);
|
||||
readonly searchControl = new FormControl<string>(this.data.search, [Validators.required]);
|
||||
readonly replaceControl = new FormControl<string>(this.data.replace);
|
||||
readonly flagControl = new FormControl<string[]>(this.optionsSelected);
|
||||
readonly forms = new FormGroup({
|
||||
search: this.searchControl,
|
||||
@ -70,8 +69,8 @@ export class FilterItemEditComponent {
|
||||
return this.regexOptions.filter(o => (flag & o.flag) > 0).map(o => o.name);
|
||||
}
|
||||
|
||||
onSaveClick(): void {
|
||||
if (this.forms.invalid || this.waitForResponse)
|
||||
save(): void {
|
||||
if (!this.forms.dirty || this.forms.invalid || this.waitForResponse)
|
||||
return;
|
||||
|
||||
this.waitForResponse = true;
|
||||
@ -110,7 +109,7 @@ export class FilterItemEditComponent {
|
||||
}
|
||||
}
|
||||
|
||||
onCancelClick(): void {
|
||||
cancel(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
<ul>
|
||||
<li>
|
||||
<span>
|
||||
{{item.search}}
|
||||
{{item().search}}
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span>
|
||||
{{item.replace}}
|
||||
{{item().replace}}
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
@ -14,7 +14,7 @@
|
||||
<button mat-menu-item
|
||||
(click)="openDialog()">Edit</button>
|
||||
<button mat-menu-item
|
||||
(click)="onDelete.emit(item)">Delete</button>
|
||||
(click)="delete()">Delete</button>
|
||||
</mat-menu>
|
||||
|
||||
<button mat-icon-button
|
||||
|
@ -9,12 +9,16 @@ ul {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-evenly;
|
||||
padding-left: 0;
|
||||
align-items: center;
|
||||
vertical-align: middle;
|
||||
|
||||
li {
|
||||
list-style-type: none;
|
||||
white-space: pre;
|
||||
text-align: start;
|
||||
text-wrap: wrap;
|
||||
padding: 0;
|
||||
|
||||
span {
|
||||
overflow: hidden;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Component, EventEmitter, inject, Input, OnInit, Output } from '@angular/core';
|
||||
import { Filter, FilterFlag } from '../../shared/models/filter';
|
||||
import { Component, inject, input } from '@angular/core';
|
||||
import { Filter } from '../../shared/models/filter';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
@ -11,36 +11,28 @@ import { HermesClientService } from '../../hermes-client.service';
|
||||
@Component({
|
||||
selector: 'tts-filter-item',
|
||||
standalone: true,
|
||||
imports: [MatButtonModule, MatCardModule, MatMenuModule, MatIconModule],
|
||||
imports: [
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
MatMenuModule,
|
||||
MatIconModule
|
||||
],
|
||||
templateUrl: './filter-item.component.html',
|
||||
styleUrl: './filter-item.component.scss'
|
||||
})
|
||||
export class FilterItemComponent implements OnInit {
|
||||
@Input() item: Filter = { id: "", user_id: "", search: "", replace: "", flag: FilterFlag.None };
|
||||
@Output() onDelete = new EventEmitter<Filter>();
|
||||
readonly client = inject(HermesClientService);
|
||||
readonly dialog = inject(MatDialog);
|
||||
private loaded = false;
|
||||
export class FilterItemComponent {
|
||||
item = input.required<Filter>();
|
||||
|
||||
private readonly client = inject(HermesClientService);
|
||||
private readonly dialog = inject(MatDialog);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loaded = true;
|
||||
delete() {
|
||||
this.client.deleteTTSFilter(this.item().id);
|
||||
}
|
||||
|
||||
openDialog(): void {
|
||||
if (!this.loaded)
|
||||
return;
|
||||
|
||||
const dialogRef = this.dialog.open(FilterItemEditComponent, {
|
||||
data: { id: this.item.id, search: this.item.search, replace: this.item.replace, flag: this.item.flag },
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result: Filter) => {
|
||||
if (result) {
|
||||
this.item.search = result.search;
|
||||
this.item.replace = result.replace;
|
||||
this.item.flag = result.flag;
|
||||
}
|
||||
this.dialog.open(FilterItemEditComponent, {
|
||||
data: { id: this.item().id, search: this.item().search, replace: this.item().replace, flag: this.item().flag },
|
||||
});
|
||||
}
|
||||
}
|
@ -2,15 +2,14 @@
|
||||
<ul class="data">
|
||||
<li>
|
||||
<ul class="header">
|
||||
<li>Search</li>
|
||||
<li>Replace</li>
|
||||
<li>Search for...</li>
|
||||
<li>Replace with...</li>
|
||||
<li></li>
|
||||
</ul>
|
||||
</li>
|
||||
@for (filter of filters; track $index) {
|
||||
@for (filter of filters(); track $index) {
|
||||
<li>
|
||||
<tts-filter-item [item]="filter"
|
||||
(onDelete)="deleteFilter($event)" />
|
||||
<tts-filter-item [item]="filter" />
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
|
@ -6,7 +6,7 @@ ul.data {
|
||||
>li {
|
||||
display: block;
|
||||
list-style-type: none;
|
||||
padding: 0.75em 1em;
|
||||
padding: 0.5em 1em;
|
||||
border-bottom: 1px solid #aaaaaa;
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, inject, Input } from '@angular/core';
|
||||
import { Component, inject, input, Input } from '@angular/core';
|
||||
import { FilterItemComponent } from '../filter-item/filter-item.component';
|
||||
import { Filter } from '../../shared/models/filter';
|
||||
import { HermesClientService } from '../../hermes-client.service';
|
||||
@ -11,10 +11,7 @@ import { HermesClientService } from '../../hermes-client.service';
|
||||
styleUrl: './filter-list.component.scss'
|
||||
})
|
||||
export class FilterListComponent {
|
||||
@Input() filters: Filter[] = [];
|
||||
client = inject(HermesClientService);
|
||||
private readonly client = inject(HermesClientService);
|
||||
|
||||
deleteFilter(e: any): void {
|
||||
this.client.deleteTTSFilter(e.id);
|
||||
}
|
||||
filters = input.required<Filter[]>();
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
article {
|
||||
@ -11,6 +10,13 @@ article {
|
||||
}
|
||||
|
||||
.grow {
|
||||
flex-grow: 1;
|
||||
width: 800px;
|
||||
height: 60vh;
|
||||
align-items: center;
|
||||
align-self: center;
|
||||
overflow: auto;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-evenly;
|
||||
border: 1px solid grey;
|
||||
border-radius: 15px;
|
||||
}
|
@ -1,11 +1,9 @@
|
||||
import { Component, inject, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
|
||||
import { Component, inject, OnDestroy } from '@angular/core';
|
||||
import { FilterListComponent } from "../filter-list/filter-list.component";
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { HermesClientService } from '../../hermes-client.service';
|
||||
import { Filter, FilterFlag } from '../../shared/models/filter';
|
||||
import { isPlatformBrowser } from '@angular/common';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { FilterItemEditComponent } from '../filter-item-edit/filter-item-edit.component';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import TtsFilterService from '../../shared/services/tts-filter.service';
|
||||
@ -14,84 +12,37 @@ import { Subscription } from 'rxjs';
|
||||
@Component({
|
||||
selector: 'filters',
|
||||
standalone: true,
|
||||
imports: [FilterListComponent, MatButtonModule, MatIconModule],
|
||||
imports: [
|
||||
FilterListComponent,
|
||||
MatButtonModule,
|
||||
MatIconModule
|
||||
],
|
||||
templateUrl: './filters.component.html',
|
||||
styleUrl: './filters.component.scss'
|
||||
})
|
||||
export class FiltersComponent implements OnInit, OnDestroy {
|
||||
export class FiltersComponent implements OnDestroy {
|
||||
private readonly filterService = inject(TtsFilterService);
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly subscriptions: Subscription[] = [];
|
||||
private isBrowser: boolean;
|
||||
private readonly subscriptions: (Subscription | undefined)[] = [];
|
||||
|
||||
readonly dialog = inject(MatDialog);
|
||||
items: Filter[];
|
||||
|
||||
items: Filter[] = [];
|
||||
|
||||
|
||||
constructor(private client: HermesClientService, private router: Router, @Inject(PLATFORM_ID) private platformId: Object) {
|
||||
this.isBrowser = isPlatformBrowser(this.platformId);
|
||||
this.items = [];
|
||||
constructor() {
|
||||
this.route.data.subscribe(data => this.items = data['filters'] || []);
|
||||
|
||||
this.route.data.subscribe(data => {
|
||||
if (!data['filters'])
|
||||
return;
|
||||
|
||||
this.items = [...data['filters']];
|
||||
});
|
||||
let subscription = this.filterService.create$?.subscribe(d => {
|
||||
if (d.error || d.request.nounce != null && d.request.nounce.startsWith(this.client.session_id))
|
||||
return;
|
||||
|
||||
this.items.push(d.data);
|
||||
});
|
||||
if (subscription)
|
||||
this.subscriptions.push(subscription);
|
||||
|
||||
subscription = this.filterService.update$?.subscribe(d => {
|
||||
if (d.error || d.request.nounce != null && d.request.nounce.startsWith(this.client.session_id))
|
||||
return;
|
||||
|
||||
const filter = this.items.find(f => f.id == d.data.id);
|
||||
if (filter) {
|
||||
filter.search = d.data.search;
|
||||
filter.replace = d.data.replace;
|
||||
filter.flag = d.data.flag || 0;
|
||||
}
|
||||
});
|
||||
if (subscription)
|
||||
this.subscriptions.push(subscription);
|
||||
|
||||
subscription = this.filterService.delete$?.subscribe(d => {
|
||||
if (d.error)
|
||||
return;
|
||||
|
||||
this.items = this.items.filter(a => a.id != d.request.data.id);
|
||||
});
|
||||
if (subscription)
|
||||
this.subscriptions.push(subscription);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (!this.isBrowser)
|
||||
return;
|
||||
|
||||
if (!this.client.logged_in) {
|
||||
this.router.navigate(["tts-login"]);
|
||||
return;
|
||||
}
|
||||
this.subscriptions.push(this.filterService.delete$?.subscribe(d => this.items = this.items.filter(a => a.id != d.request.data.id)));
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.subscriptions.forEach(s => s.unsubscribe());
|
||||
this.subscriptions.filter(s => !!s).forEach(s => s.unsubscribe());
|
||||
}
|
||||
|
||||
openDialog(): void {
|
||||
const dialogRef = this.dialog.open(FilterItemEditComponent, {
|
||||
this.dialog.open(FilterItemEditComponent, {
|
||||
data: { id: '', user_id: '', search: '', replace: '', flag: FilterFlag.None },
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result: Filter) => {
|
||||
if (result)
|
||||
this.items.push(result);
|
||||
});
|
||||
}
|
||||
}
|
@ -1,9 +1,10 @@
|
||||
import { isPlatformBrowser } from '@angular/common';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Component, Inject, OnInit, PLATFORM_ID } from '@angular/core';
|
||||
import { Component, inject, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ApiAuthenticationService } from '../shared/services/api/api-authentication.service';
|
||||
import { environment } from '../../environments/environment';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-twitch-auth-callback',
|
||||
@ -12,20 +13,26 @@ import { environment } from '../../environments/environment';
|
||||
templateUrl: './twitch-auth-callback.component.html',
|
||||
styleUrl: './twitch-auth-callback.component.scss'
|
||||
})
|
||||
export class TwitchAuthCallbackComponent implements OnInit {
|
||||
private isBrowser: boolean;
|
||||
export class TwitchAuthCallbackComponent implements OnInit, OnDestroy {
|
||||
private readonly auth = inject(ApiAuthenticationService);
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly router = inject(Router);
|
||||
private readonly subscriptions: (Subscription | null)[] = [];
|
||||
|
||||
constructor(private http: HttpClient, private auth: ApiAuthenticationService, private route: ActivatedRoute, private router: Router, @Inject(PLATFORM_ID) private platformId: Object) {
|
||||
this.isBrowser = isPlatformBrowser(this.platformId)
|
||||
}
|
||||
constructor(@Inject(PLATFORM_ID) private platformId: Object) { }
|
||||
|
||||
async ngOnInit(): Promise<any> {
|
||||
if (!this.isBrowser) {
|
||||
if (!isPlatformBrowser(this.platformId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.auth.isAuthenticated()) {
|
||||
await this.router.navigate(['tts-login']);
|
||||
if (!this.auth.isAuthenticated() && localStorage.getItem('jwt')) {
|
||||
localStorage.removeItem('jwt');
|
||||
}
|
||||
|
||||
if (this.auth.isAuthenticated() || localStorage.getItem('jwt')) {
|
||||
await this.router.navigate(['policies']);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -43,20 +50,22 @@ export class TwitchAuthCallbackComponent implements OnInit {
|
||||
}
|
||||
|
||||
this.http.post(environment.API_HOST + '/auth/twitch/callback', { code, scope, state })
|
||||
.subscribe(async (response: any) => {
|
||||
if (!response?.authenticated) {
|
||||
await this.router.navigate(['login'], {
|
||||
.subscribe({
|
||||
next: async (response: any) => {
|
||||
this.auth.update(response.token);
|
||||
},
|
||||
error: async () => await this.router.navigate(['login'], {
|
||||
queryParams: {
|
||||
error: 'callback_issue'
|
||||
error: 'callback_issue_twitch'
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem('jwt', response.token);
|
||||
this.auth.update();
|
||||
|
||||
await this.router.navigate(['tts-login']);
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
for (let subscription of this.subscriptions) {
|
||||
if (subscription)
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import { Group } from '../../shared/models/group';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { environment } from '../../../environments/environment';
|
||||
|
||||
@Component({
|
||||
selector: 'app-twitch-user-item-add',
|
||||
@ -41,7 +42,7 @@ export class TwitchUserItemAddComponent implements OnInit {
|
||||
}
|
||||
|
||||
submit() {
|
||||
if (this.usernameControl.invalid || this.waitForResponse || !this.client.api_key) {
|
||||
if (!this.usernameControl.dirty || this.usernameControl.invalid || this.waitForResponse || !this.client.api_key) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -49,7 +50,7 @@ export class TwitchUserItemAddComponent implements OnInit {
|
||||
this.responseError = undefined;
|
||||
|
||||
const username = this.usernameControl.value!.toLowerCase();
|
||||
this.http.get('/api/auth/twitch/users?login=' + username, {
|
||||
this.http.get(environment.API_HOST + '/auth/twitch/users?login=' + username, {
|
||||
headers: {
|
||||
'x-api-key': this.client.api_key,
|
||||
}
|
||||
|
@ -1,6 +1,13 @@
|
||||
@use '@angular/material' as mat;
|
||||
|
||||
ul {
|
||||
background-color: rgb(202, 68, 255);
|
||||
border-radius: 15px;
|
||||
margin: 0 0;
|
||||
padding: 0;
|
||||
max-width: 500px;
|
||||
overflow: hidden;
|
||||
|
||||
@include mat.all-component-densities(-5);
|
||||
|
||||
@include mat.form-field-overrides((
|
||||
@ -8,13 +15,6 @@ ul {
|
||||
outlined-focus-label-text-color: rgb(155, 57, 194),
|
||||
outlined-focus-outline-color: rgb(155, 57, 194),
|
||||
));
|
||||
|
||||
background-color: rgb(202, 68, 255);
|
||||
border-radius: 15px;
|
||||
margin: 0 0;
|
||||
padding: 0;
|
||||
max-width: 500px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
ul li {
|
||||
|
@ -7,6 +7,10 @@
|
||||
<base href="/">
|
||||
<meta name="viewport"
|
||||
content="width=device-width, initial-scale=1">
|
||||
<meta name="robots"
|
||||
content="noindex,nofollow">
|
||||
<meta name="googlebot"
|
||||
content="noindex,nofollow">
|
||||
<link rel="icon"
|
||||
type="image/x-icon"
|
||||
href="favicon.ico">
|
||||
@ -14,8 +18,10 @@
|
||||
rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
||||
rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap"
|
||||
rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
||||
rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body class="mat-typography">
|
||||
|
@ -12,6 +12,7 @@ html {
|
||||
));
|
||||
}
|
||||
|
||||
:root,
|
||||
html,
|
||||
body,
|
||||
main {
|
||||
@ -20,9 +21,9 @@ main {
|
||||
|
||||
html,
|
||||
body,
|
||||
div.below-topbar,
|
||||
main,
|
||||
content,
|
||||
nav {
|
||||
content {
|
||||
background-color: var(--mat-sys-background);
|
||||
color: var(--mat-sys-on-background);
|
||||
}
|
||||
@ -99,7 +100,7 @@ body {
|
||||
.confirm {
|
||||
@include mat.button-overrides(( //text-state-layer-color: rgb(52, 255, 62),
|
||||
text-label-text-color: rgb(71, 218, 78),
|
||||
text-disabled-label-text-color: rgb(71, 218, 78),
|
||||
text-disabled-label-text-color: rgb(107, 107, 107),
|
||||
));
|
||||
}
|
||||
|
||||
@ -120,10 +121,13 @@ body {
|
||||
.danger {
|
||||
@include mat.button-overrides(( //text-state-layer-color: rgb(255, 48, 48),
|
||||
text-label-text-color: rgb(255, 52, 52),
|
||||
text-disabled-label-text-color: rgb(255, 52, 52),
|
||||
text-disabled-label-text-color: rgb(107, 107, 107),
|
||||
filled-label-text-color: rgb(255, 52, 52),
|
||||
filled-disabled-label-text-color: rgb(107, 107, 107),
|
||||
outlined-label-text-color: rgb(255, 52, 52),
|
||||
outlined-disabled-label-text-color: rgb(107, 107, 107),
|
||||
protected-label-text-color: rgb(255, 52, 52),
|
||||
protected-disabled-label-text-color: rgb(107, 107, 107),
|
||||
protected-state-layer-color: rgb(255, 75, 75),
|
||||
protected-ripple-color: rgb(255, 154, 154),
|
||||
));
|
||||
|
Reference in New Issue
Block a user