Added redemptions page. Fixed some issues. Removed some instances of console.log().
This commit is contained in:
parent
7a7fb832a0
commit
04a50f6db0
2691
package-lock.json
generated
2691
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,24 @@
|
||||
<mat-form-field>
|
||||
<mat-label>Redeemable Action</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
placeholder="Pick a Redeemable Action"
|
||||
aria-label="redeemable action"
|
||||
[formControl]="formControl"
|
||||
[matAutocomplete]="auto"
|
||||
(blur)="blur()"
|
||||
(input)="input()">
|
||||
<mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn" (optionSelected)="select($event.option.value)">
|
||||
@for (action of filteredActions; track action.name) {
|
||||
<mat-option [value]="action">{{action.name}}</mat-option>
|
||||
}
|
||||
</mat-autocomplete>
|
||||
@if (!search && formControl.invalid && (formControl.dirty || formControl.touched)) {
|
||||
@for (error of errorMessageKeys; track $index) {
|
||||
@if (formControl.hasError(error)) {
|
||||
<small class="error">{{errorMessages[error]}}</small>
|
||||
}
|
||||
}
|
||||
}
|
||||
</mat-form-field>
|
@ -0,0 +1,4 @@
|
||||
.error {
|
||||
display: block;
|
||||
color: #ba1a1a;
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ActionDropdownComponent } from './action-dropdown.component';
|
||||
|
||||
describe('ActionDropdownComponent', () => {
|
||||
let component: ActionDropdownComponent;
|
||||
let fixture: ComponentFixture<ActionDropdownComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ActionDropdownComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ActionDropdownComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
90
src/app/actions/action-dropdown/action-dropdown.component.ts
Normal file
90
src/app/actions/action-dropdown/action-dropdown.component.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import { Component, EventEmitter, inject, input, Input, OnInit, Output } from '@angular/core';
|
||||
import { FormControl, ReactiveFormsModule, Validators } 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({
|
||||
selector: 'action-dropdown',
|
||||
imports: [MatAutocompleteModule, MatFormFieldModule, MatInputModule, ReactiveFormsModule],
|
||||
templateUrl: './action-dropdown.component.html',
|
||||
styleUrl: './action-dropdown.component.scss'
|
||||
})
|
||||
export class ActionDropdownComponent implements OnInit {
|
||||
@Input() formControl = new FormControl<RedeemableAction | string | undefined>(undefined);
|
||||
@Input() errorMessages: { [errorKey: string]: string } = {}
|
||||
|
||||
@Input() search: boolean = false;
|
||||
@Input() actions: RedeemableAction[] = [];
|
||||
@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);
|
||||
|
||||
if (!this.action)
|
||||
return;
|
||||
|
||||
const action = this.actions.find(r => r.name == this.action);
|
||||
this.formControl.setValue(action);
|
||||
}
|
||||
|
||||
get filteredActions() {
|
||||
const value = this.formControl.value;
|
||||
if (typeof value == 'string') {
|
||||
return this.actions.filter(r => r.name.toLowerCase().includes(value.toLowerCase()));
|
||||
}
|
||||
return this.actions;
|
||||
}
|
||||
|
||||
select(event: RedeemableAction) {
|
||||
this.actionChange.emit(event.name);
|
||||
}
|
||||
|
||||
input() {
|
||||
if (this.search && typeof this.formControl.value == 'string') {
|
||||
this.actionChange.emit(this.formControl.value);
|
||||
}
|
||||
}
|
||||
|
||||
blur() {
|
||||
if (!this.search && typeof this.formControl.value == 'string') {
|
||||
const name = this.formControl.value;
|
||||
const nameLower = name.toLowerCase();
|
||||
let newValue: RedeemableAction | undefined = undefined;
|
||||
const insenstiveActions = this.filteredActions.filter(a => a.name.toLowerCase() == nameLower);
|
||||
if (insenstiveActions.length > 1) {
|
||||
const sensitiveAction = insenstiveActions.find(a => a.name == name);
|
||||
newValue = sensitiveAction ?? undefined;
|
||||
} else if (insenstiveActions.length == 1) {
|
||||
newValue = insenstiveActions[0];
|
||||
}
|
||||
|
||||
if (newValue && newValue.name != this.formControl.value) {
|
||||
this.formControl.setValue(newValue);
|
||||
this.actionChange.emit(newValue.name);
|
||||
} else if (!newValue)
|
||||
this.actionChange.emit(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
displayFn(value: any) {
|
||||
return value?.name;
|
||||
}
|
||||
}
|
@ -67,10 +67,6 @@ export class ActionsComponent implements OnInit {
|
||||
}
|
||||
});
|
||||
this.client.subscribeToRequests('delete_redeemable_action', d => {
|
||||
// if (d.request.nounce != null && d.request.nounce.startsWith(this.client.session_id)) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
this.items = this.actions.filter(a => a.name != d.request.data.name);
|
||||
});
|
||||
|
||||
|
@ -53,10 +53,6 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}));
|
||||
|
||||
const connection = this.client.connect();
|
||||
if (connection) {
|
||||
this.addSubscription(connection);
|
||||
}
|
||||
this.ngZone.runOutsideAngular(() => setInterval(() => this.client.heartbeat(), 15000));
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,10 @@ import { FiltersComponent } from './tts-filters/filters/filters.component';
|
||||
import { AuthAdminGuard } from './shared/auth/auth.admin.guard';
|
||||
import { AuthVisitorGuard } from './shared/auth/auth.visitor.guard';
|
||||
import { ActionsComponent } from './actions/actions/actions.component';
|
||||
import { RedemptionsComponent } from './redemptions/redemptions/redemptions.component';
|
||||
import RedemptionResolver from './shared/resolvers/redemption-resolver';
|
||||
import TwitchRedemptionResolver from './shared/resolvers/twitch-redemption-resolver';
|
||||
import RedeemableActionResolver from './shared/resolvers/redeemable-action-resolver';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
@ -24,6 +28,19 @@ export const routes: Routes = [
|
||||
path: 'actions',
|
||||
component: ActionsComponent,
|
||||
canActivate: [AuthAdminGuard],
|
||||
resolve: {
|
||||
redeemableActions: RedeemableActionResolver,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'redemptions',
|
||||
component: RedemptionsComponent,
|
||||
canActivate: [AuthAdminGuard],
|
||||
resolve: {
|
||||
redeemableActions: RedeemableActionResolver,
|
||||
redemptions: RedemptionResolver,
|
||||
twitchRedemptions: TwitchRedemptionResolver,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'login',
|
||||
|
@ -10,72 +10,72 @@ import { HermesClientService } from '../../hermes-client.service';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'impersonation',
|
||||
standalone: true,
|
||||
imports: [MatCardModule, MatSelectModule],
|
||||
templateUrl: './impersonation.component.html',
|
||||
styleUrl: './impersonation.component.scss'
|
||||
selector: 'impersonation',
|
||||
standalone: true,
|
||||
imports: [MatCardModule, MatSelectModule],
|
||||
templateUrl: './impersonation.component.html',
|
||||
styleUrl: './impersonation.component.scss'
|
||||
})
|
||||
export class ImpersonationComponent implements OnInit {
|
||||
impersonated: string | undefined;
|
||||
users: { id: string, name: string }[];
|
||||
impersonated: string | undefined;
|
||||
users: { id: string, name: string }[];
|
||||
|
||||
constructor(private hermes: HermesClientService, private auth: ApiAuthenticationService, private router: Router, private events: EventService, private http: HttpClient, @Inject(PLATFORM_ID) private platformId: Object) {
|
||||
this.users = []
|
||||
constructor(private hermes: HermesClientService, private auth: ApiAuthenticationService, private router: Router, private events: EventService, private http: HttpClient, @Inject(PLATFORM_ID) private platformId: Object) {
|
||||
this.users = []
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (!isPlatformBrowser(this.platformId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (!isPlatformBrowser(this.platformId)) {
|
||||
return;
|
||||
this.http.get(environment.API_HOST + '/admin/users', {
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('jwt')
|
||||
}
|
||||
}).subscribe((data: any) => {
|
||||
this.users = data.filter((d: any) => d.name != this.auth.getUsername());
|
||||
const id = this.auth.getImpersonatedId();
|
||||
if (id && this.users.find(u => u.id == id)) {
|
||||
this.impersonated = id;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public isAdmin() {
|
||||
return this.auth.isAdmin();
|
||||
}
|
||||
|
||||
public getUsername() {
|
||||
return this.auth.getUsername();
|
||||
}
|
||||
|
||||
public onChange(e: any) {
|
||||
if (!e.value) {
|
||||
this.http.delete(environment.API_HOST + '/admin/impersonate', {
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('jwt')
|
||||
},
|
||||
body: {
|
||||
impersonation: e.value
|
||||
}
|
||||
|
||||
this.http.get(environment.API_HOST + '/admin/users', {
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('jwt')
|
||||
}
|
||||
}).subscribe((data: any) => {
|
||||
this.users = data.filter((d: any) => d.name != this.auth.getUsername());
|
||||
const id = this.auth.getImpersonatedId();
|
||||
if (id && this.users.find(u => u.id == id)) {
|
||||
this.impersonated = id;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public isAdmin() {
|
||||
return this.auth.isAdmin();
|
||||
}
|
||||
|
||||
public getUsername() {
|
||||
return this.auth.getUsername();
|
||||
}
|
||||
|
||||
public onChange(e: any) {
|
||||
if (!e.value) {
|
||||
this.http.delete(environment.API_HOST + '/admin/impersonate', {
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('jwt')
|
||||
},
|
||||
body: {
|
||||
impersonation: e.value
|
||||
}
|
||||
}).subscribe(async (data: any) => {
|
||||
this.hermes.disconnect();
|
||||
this.events.emit('impersonation', e.value);
|
||||
await this.router.navigate(['tts-login']);
|
||||
});
|
||||
} else {
|
||||
this.http.put(environment.API_HOST + '/admin/impersonate', {
|
||||
impersonation: e.value
|
||||
}, {
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('jwt')
|
||||
}
|
||||
}).subscribe(async (data: any) => {
|
||||
this.hermes.disconnect();
|
||||
this.events.emit('impersonation', e.value);
|
||||
await this.router.navigate(['tts-login']);
|
||||
});
|
||||
}).subscribe(async (data: any) => {
|
||||
this.hermes.disconnect();
|
||||
this.events.emit('impersonation', e.value);
|
||||
await this.router.navigate(['tts-login']);
|
||||
});
|
||||
} else {
|
||||
this.http.put(environment.API_HOST + '/admin/impersonate', {
|
||||
impersonation: e.value
|
||||
}, {
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('jwt')
|
||||
}
|
||||
}).subscribe(async (data: any) => {
|
||||
this.hermes.disconnect();
|
||||
this.events.emit('impersonation', e.value);
|
||||
await this.router.navigate(['tts-login']);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { HermesSocketService } from './hermes-socket.service';
|
||||
import EventService from './shared/services/EventService';
|
||||
import { Observable } from 'rxjs';
|
||||
import { filter, map, Observable } from 'rxjs';
|
||||
|
||||
export interface Message {
|
||||
d: object,
|
||||
@ -48,7 +48,21 @@ export class HermesClientService {
|
||||
this.events.emit('tts_logoff', null);
|
||||
}
|
||||
|
||||
public first(predicate: (data: any) => boolean): Observable<any> | null {
|
||||
public filter(predicate: (data: any) => boolean): Observable<any>|undefined {
|
||||
return this.socket.get$()?.pipe(
|
||||
filter(predicate),
|
||||
map(d => d.d)
|
||||
);
|
||||
}
|
||||
|
||||
public filterByRequestType(requestName: string): Observable<any>|undefined {
|
||||
return this.socket.get$()?.pipe(
|
||||
filter(d => d.op == 4 && d.d.request.type === requestName),
|
||||
map(d => d.d)
|
||||
);
|
||||
}
|
||||
|
||||
public first(predicate: (data: any) => boolean): Observable<any> {
|
||||
return this.socket.first(predicate);
|
||||
}
|
||||
|
||||
@ -101,6 +115,18 @@ export class HermesClientService {
|
||||
});
|
||||
}
|
||||
|
||||
public createRedemption(twitchRedemptionId: string, actionName: string, order: number) {
|
||||
if (!this.logged_in)
|
||||
return;
|
||||
|
||||
this.send(3, {
|
||||
request_id: null,
|
||||
type: "create_redemption",
|
||||
data: { redemption: twitchRedemptionId, action: actionName, order },
|
||||
nounce: this.session_id,
|
||||
});
|
||||
}
|
||||
|
||||
public createTTSFilter(search: string, replace: string) {
|
||||
if (!this.logged_in)
|
||||
return;
|
||||
@ -135,6 +161,18 @@ export class HermesClientService {
|
||||
});
|
||||
}
|
||||
|
||||
public deleteRedemption(id: string) {
|
||||
if (!this.logged_in)
|
||||
return;
|
||||
|
||||
this.send(3, {
|
||||
request_id: null,
|
||||
type: "delete_redemption",
|
||||
data: { id },
|
||||
nounce: this.session_id,
|
||||
});
|
||||
}
|
||||
|
||||
public deleteTTSFilter(id: string) {
|
||||
if (!this.logged_in)
|
||||
return;
|
||||
@ -191,6 +229,17 @@ export class HermesClientService {
|
||||
});
|
||||
}
|
||||
|
||||
public fetchRedemptions() {
|
||||
if (!this.logged_in)
|
||||
return;
|
||||
|
||||
this.send(3, {
|
||||
request_id: null,
|
||||
type: "get_redemptions",
|
||||
data: null,
|
||||
});
|
||||
}
|
||||
|
||||
public heartbeat() {
|
||||
const date = new Date()
|
||||
this.send(0, {
|
||||
@ -240,6 +289,18 @@ export class HermesClientService {
|
||||
});
|
||||
}
|
||||
|
||||
public updateRedemption(id: string, twitchRedemptionId: string, actionName: string, order: number) {
|
||||
if (!this.logged_in)
|
||||
return;
|
||||
|
||||
this.send(3, {
|
||||
request_id: null,
|
||||
type: "update_redemption",
|
||||
data: { id, redemption: twitchRedemptionId, action: actionName, order, state: true },
|
||||
nounce: this.session_id,
|
||||
});
|
||||
}
|
||||
|
||||
public updateTTSFilter(id: string, search: string, replace: string) {
|
||||
if (!this.logged_in)
|
||||
return;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { OnInit, Injectable } from '@angular/core';
|
||||
import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
|
||||
import { catchError, first, timeout } from 'rxjs/operators';
|
||||
import { catchError, filter, first, timeout } from 'rxjs/operators';
|
||||
import { environment } from '../environments/environment';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
|
||||
@ -22,26 +22,30 @@ export class HermesSocketService implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
public first(predicate: (data: any) => boolean): Observable<any>|null {
|
||||
public first(predicate: (data: any) => boolean): Observable<any> {
|
||||
if (!this.socket || this.socket.closed)
|
||||
return null;
|
||||
return new Observable().pipe(timeout(3000), catchError((e) => throwError(() => 'No response after 3 seconds.')));
|
||||
|
||||
return this.socket.pipe(timeout(3000), catchError((e) => throwError(() => 'No response after 3 seconds.')), first(predicate));
|
||||
}
|
||||
|
||||
private getNewWebSocket() {
|
||||
private getNewWebSocket(): WebSocketSubject<any> {
|
||||
return webSocket({
|
||||
url: environment.WSS_ENDPOINT
|
||||
});
|
||||
}
|
||||
|
||||
public sendMessage(msg: any) {
|
||||
public sendMessage(msg: any): void {
|
||||
if (!this.socket || this.socket.closed)
|
||||
return;
|
||||
|
||||
this.socket.next(msg);
|
||||
}
|
||||
|
||||
public get$(): Observable<any>|undefined {
|
||||
return this.socket?.asObservable();
|
||||
}
|
||||
|
||||
public subscribe(subscriptions: any) {
|
||||
if (!this.socket || this.socket.closed)
|
||||
return;
|
||||
|
@ -26,5 +26,10 @@
|
||||
Actions
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a routerLink="/redemptions" routerLinkActive="active" *ngIf="isLoggedIn() && isTTSLoggedIn() && isAdmin()">
|
||||
Redemptions
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
@ -44,7 +44,6 @@ export class PolicyTableComponent implements OnInit, OnDestroy {
|
||||
for (let policy of response.data) {
|
||||
this.policies.push(new Policy(policy.id, policy.group_id, policy.path, policy.usage, policy.span, "", false, false));
|
||||
}
|
||||
console.log('getting policies', response.data);
|
||||
this.table.renderRows();
|
||||
} else if (response.request.type == "create_policy") {
|
||||
const policy = this.policies.find(p => this.groups[response.data.group_id].name == p.temp_group_name && p.path == response.data.path);
|
||||
|
@ -0,0 +1,57 @@
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title>
|
||||
{{redemption.id ? "Edit" : "Add"}} Redemption
|
||||
</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<twitch-redemption-dropdown
|
||||
ngDefaultControl
|
||||
[formControl]="redemptionFormControl"
|
||||
[errorMessages]="redemptionErrorMessages"
|
||||
[twitchRedemptions]="twitchRedemptions"
|
||||
[(twitchRedemptionId)]="redemption.redemption_id" />
|
||||
|
||||
<action-dropdown
|
||||
ngDefaultControl
|
||||
[formControl]="actionFormControl"
|
||||
[errorMessages]="actionErrorMessages"
|
||||
[actions]="redeemableActions"
|
||||
[(action)]="redemption.action_name" />
|
||||
|
||||
<mat-form-field>
|
||||
<mat-label>Order</mat-label>
|
||||
<input matInput type="number" [formControl]="orderFormControl" [value]="redemption.order" />
|
||||
@if (orderFormControl.invalid && (orderFormControl.dirty || orderFormControl.touched)) {
|
||||
@for (error of orderErrorMessageKeys; track $index) {
|
||||
@if (orderFormControl.hasError(error)) {
|
||||
<small class="error">{{orderErrorMessages[error]}}</small>
|
||||
}
|
||||
}
|
||||
}
|
||||
</mat-form-field>
|
||||
<div class="buttons">
|
||||
<button
|
||||
mat-icon-button
|
||||
class="save"
|
||||
[disabled]="waitForResponse || formGroups.invalid"
|
||||
(click)="save()">
|
||||
<mat-icon>save</mat-icon>
|
||||
</button>
|
||||
@if (redemption.id) {
|
||||
<button
|
||||
mat-icon-button
|
||||
class="delete"
|
||||
[disabled]="waitForResponse"
|
||||
(click)="delete()">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</mat-card-content>
|
||||
@if (responseError) {
|
||||
<mat-card-footer>
|
||||
<small class="error below">{{responseError}}</small>
|
||||
</mat-card-footer>
|
||||
}
|
||||
</mat-card>
|
@ -0,0 +1,56 @@
|
||||
.mat-mdc-card {
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
||||
.mat-mdc-card-content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
column-gap: 1em;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
margin: -0.75em;
|
||||
}
|
||||
|
||||
button,
|
||||
.mdc-button {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.error {
|
||||
display: block;
|
||||
color: #ba1a1a;
|
||||
}
|
||||
|
||||
.below {
|
||||
display: block;
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
@media (max-width:1000px) {
|
||||
.mat-mdc-card-content {
|
||||
flex-direction: column;
|
||||
margin-bottom: 0.5em;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
button ~ button {
|
||||
margin-left: 2em;
|
||||
}
|
||||
|
||||
button {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.delete {
|
||||
background-color: rgb(255, 152, 152);
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { RedemptionItemEditComponent } from './redemption-item-edit.component';
|
||||
|
||||
describe('RedemptionItemEditComponent', () => {
|
||||
let component: RedemptionItemEditComponent;
|
||||
let fixture: ComponentFixture<RedemptionItemEditComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [RedemptionItemEditComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(RedemptionItemEditComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,145 @@
|
||||
import { Component, inject, Input, model, OnInit, signal } from '@angular/core';
|
||||
import Redemption from '../../shared/models/redemption';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { ActionDropdownComponent } from '../../actions/action-dropdown/action-dropdown.component';
|
||||
import { TwitchRedemptionDropdownComponent } from '../twitch-redemption-dropdown/twitch-redemption-dropdown.component';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { HermesClientService } from '../../hermes-client.service';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import TwitchRedemption from '../../shared/models/twitch-redemption';
|
||||
import RedeemableAction from '../../shared/models/redeemable_action';
|
||||
import { 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',
|
||||
imports: [
|
||||
ActionDropdownComponent,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
MatFormFieldModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
ReactiveFormsModule,
|
||||
TwitchRedemptionDropdownComponent,
|
||||
],
|
||||
templateUrl: './redemption-item-edit.component.html',
|
||||
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);
|
||||
|
||||
redemptionFormControl = new FormControl<TwitchRedemption | string | undefined>(undefined, [Validators.required, createTypeValidator('Object')]);
|
||||
redemptionErrorMessages: { [errorKey: string]: string } = {
|
||||
'required': 'This field is required.',
|
||||
'invalidType': 'Select a value from the dropdown.',
|
||||
};
|
||||
|
||||
actionFormControl = new FormControl<RedeemableAction | string | undefined>(undefined, [Validators.required, createTypeValidator('Object')]);
|
||||
actionErrorMessages: { [errorKey: string]: string } = {
|
||||
'required': 'This field is required.',
|
||||
'invalidType': 'Select a value from the dropdown.',
|
||||
};
|
||||
|
||||
orderFormControl = new FormControl(0, [Validators.required, Validators.min(0), Validators.max(99), integerValidator]);
|
||||
orderErrorMessages: { [errorKey: string]: string } = {
|
||||
'required': 'This field is required.',
|
||||
'min': 'The value must be at least 0.',
|
||||
'max': 'The value must be at most 99.',
|
||||
'integer': 'The value must be a whole number.',
|
||||
};
|
||||
orderErrorMessageKeys: string[] = [];
|
||||
|
||||
formGroups = new FormGroup({
|
||||
twitchRedemption: this.redemptionFormControl,
|
||||
redeemableAction: this.actionFormControl,
|
||||
order: this.orderFormControl,
|
||||
});
|
||||
|
||||
redemption: Redemption = { id: '', user_id: '', redemption_id: '', action_name: '', order: 0, state: true };
|
||||
twitchRedemptions: 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.orderErrorMessageKeys = Object.keys(this.orderErrorMessages);
|
||||
}
|
||||
|
||||
|
||||
delete() {
|
||||
const id = this.redemption.id;
|
||||
this.waitForResponse = true
|
||||
this.client.first((d: any) => d.op == 4 && d.d.request.type == 'delete_redemption' && d.d.request.data.id == id)
|
||||
?.subscribe({
|
||||
next: (d) => {
|
||||
if (d.d.error) {
|
||||
this.responseError = d.d.error;
|
||||
return;
|
||||
}
|
||||
|
||||
this.dialogRef.close(id);
|
||||
},
|
||||
error: () => { this.responseError = 'Failed to receive response back from server.'; this.waitForResponse = false; },
|
||||
complete: () => this.waitForResponse = false,
|
||||
});
|
||||
this.client.deleteRedemption(id);
|
||||
}
|
||||
|
||||
save() {
|
||||
this.responseError = undefined;
|
||||
const order = this.orderFormControl.value;
|
||||
if (order == null) {
|
||||
this.responseError = 'Order must be an integer.';
|
||||
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 ?? ''))
|
||||
?.subscribe({
|
||||
next: (d) => {
|
||||
if (d.d.error) {
|
||||
this.responseError = d.d.error;
|
||||
return;
|
||||
}
|
||||
|
||||
this.redemption.order = order;
|
||||
this.dialogRef.close(d.d.data);
|
||||
},
|
||||
error: () => { this.responseError = 'Failed to receive response back from server.'; this.waitForResponse = false; },
|
||||
complete: () => this.waitForResponse = false,
|
||||
});
|
||||
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;
|
||||
return;
|
||||
}
|
||||
this.redemption.order = order;
|
||||
this.dialogRef.close(d.d.data);
|
||||
},
|
||||
error: () => { this.responseError = 'Failed to receive response back from server.'; this.waitForResponse = false; },
|
||||
complete: () => this.waitForResponse = false,
|
||||
});
|
||||
this.client.updateRedemption(this.redemption.id, this.redemption.redemption_id, this.redemption.action_name, order);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
<button mat-button class="add" (click)="add()"><mat-icon>add</mat-icon> Add Redemption</button>
|
||||
|
||||
<mat-expansion-panel class="filters-expander" (opened)="panelOpenState.set(true)" (closed)="panelOpenState.set(false)">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>Filters</mat-panel-title>
|
||||
<mat-panel-description>
|
||||
Expand for filtering options
|
||||
</mat-panel-description>
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
<div class="filters">
|
||||
<twitch-redemption-dropdown [(twitchRedemptionId)]="filter_redemption" [search]="true" />
|
||||
<action-dropdown [(action)]="filter_action_name" [search]="true" />
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
|
||||
<div class="table-container">
|
||||
<table mat-table [dataSource]="redemptions" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="twitch-redemption">
|
||||
<th mat-header-cell *matHeaderCellDef>Twitch Redemption Name</th>
|
||||
<td mat-cell *matCellDef="let redemption">{{getTwitchRedemptionNameById(redemption.redemption_id) || 'Unknown Twitch Redemption'}}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="action-name">
|
||||
<th mat-header-cell *matHeaderCellDef>Action Name</th>
|
||||
<td mat-cell *matCellDef="let redemption">{{redemption.action_name}}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="order">
|
||||
<th mat-header-cell *matHeaderCellDef>Order</th>
|
||||
<td mat-cell *matCellDef="let redemption">{{redemption.order}}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="misc">
|
||||
<th mat-header-cell *matHeaderCellDef></th>
|
||||
<td mat-cell *matCellDef="let redemption">
|
||||
<button mat-icon-button (click)="openDialog(redemption)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
</div>
|
@ -0,0 +1,27 @@
|
||||
.filters-expander {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.mat-mdc-table {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
min-width: 555px;
|
||||
height: 60vh;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.add {
|
||||
width: 100%;
|
||||
margin-top: 3em;
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { RedemptionListComponent } from './redemption-list.component';
|
||||
|
||||
describe('RedemptionListComponent', () => {
|
||||
let component: RedemptionListComponent;
|
||||
let fixture: ComponentFixture<RedemptionListComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [RedemptionListComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(RedemptionListComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
173
src/app/redemptions/redemption-list/redemption-list.component.ts
Normal file
173
src/app/redemptions/redemption-list/redemption-list.component.ts
Normal file
@ -0,0 +1,173 @@
|
||||
import { Component, inject, OnDestroy, OnInit, signal } from '@angular/core';
|
||||
import { RedemptionService } from '../../shared/services/redemption.service';
|
||||
import Redemption from '../../shared/models/redemption';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { TwitchRedemptionDropdownComponent } from "../twitch-redemption-dropdown/twitch-redemption-dropdown.component";
|
||||
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';
|
||||
|
||||
@Component({
|
||||
selector: 'redemption-list',
|
||||
imports: [
|
||||
ActionDropdownComponent,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
MatExpansionModule,
|
||||
MatFormFieldModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
MatTableModule,
|
||||
ScrollingModule,
|
||||
TwitchRedemptionDropdownComponent,
|
||||
],
|
||||
templateUrl: './redemption-list.component.html',
|
||||
styleUrl: './redemption-list.component.scss'
|
||||
})
|
||||
export class RedemptionListComponent implements OnInit, OnDestroy {
|
||||
private readonly client = inject(HermesClientService);
|
||||
private readonly redemptionService = inject(RedemptionService);
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
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(true);
|
||||
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;
|
||||
|
||||
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(): void {
|
||||
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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));
|
||||
filtered = filtered.filter(r => !actionFilter || r.action_name.toLowerCase().includes(actionFilter));
|
||||
return filtered;
|
||||
}
|
||||
|
||||
getTwitchRedemptionNameById(id: string) {
|
||||
return this._twitchRedemptionsDict[id];
|
||||
}
|
||||
|
||||
add(): void {
|
||||
this.openDialog({ id: '', user_id: '', redemption_id: '', action_name: '', order: 0, state: true });
|
||||
}
|
||||
|
||||
openDialog(redemption: Redemption): void {
|
||||
const dialogRef = 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
21
src/app/redemptions/redemptions.module.ts
Normal file
21
src/app/redemptions/redemptions.module.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RedemptionListComponent } from './redemption-list/redemption-list.component';
|
||||
import { RedemptionsComponent } from './redemptions/redemptions.component';
|
||||
import { TwitchRedemptionDropdownComponent } from './twitch-redemption-dropdown/twitch-redemption-dropdown.component';
|
||||
import { RedemptionService } from '../shared/services/redemption.service';
|
||||
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
imports: [
|
||||
RedemptionListComponent,
|
||||
RedemptionsComponent,
|
||||
TwitchRedemptionDropdownComponent,
|
||||
],
|
||||
providers: [
|
||||
RedemptionService
|
||||
]
|
||||
})
|
||||
export class RedemptionsModule { }
|
@ -0,0 +1,5 @@
|
||||
<div class="root">
|
||||
<div class="content">
|
||||
<redemption-list />
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,6 @@
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-evenly;
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { RedemptionsComponent } from './redemptions.component';
|
||||
|
||||
describe('RedemptionsComponent', () => {
|
||||
let component: RedemptionsComponent;
|
||||
let fixture: ComponentFixture<RedemptionsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [RedemptionsComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(RedemptionsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
26
src/app/redemptions/redemptions/redemptions.component.ts
Normal file
26
src/app/redemptions/redemptions/redemptions.component.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { Component, inject, OnInit } 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';
|
||||
|
||||
@Component({
|
||||
selector: 'redemptions',
|
||||
imports: [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 {
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
<mat-form-field>
|
||||
<mat-label>Twitch Redemption</mat-label>
|
||||
<input type="text" matInput placeholder="Pick a Twitch redemption" aria-label="twitch redemption"
|
||||
[formControl]="formControl" [matAutocomplete]="auto" (blur)="blur()" (input)="input()">
|
||||
<mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn" (optionSelected)="select($event.option.value)">
|
||||
@for (redemption of filteredRedemptions; track redemption.id) {
|
||||
<mat-option [value]="redemption">{{redemption.title}}</mat-option>
|
||||
}
|
||||
</mat-autocomplete>
|
||||
@if (!search && formControl.invalid && (formControl.dirty || formControl.touched)) {
|
||||
@for (error of errorMessageKeys; track $index) {
|
||||
@if (formControl.hasError(error)) {
|
||||
<small class="error">{{errorMessages[error]}}</small>
|
||||
}
|
||||
}
|
||||
}
|
||||
</mat-form-field>
|
@ -0,0 +1,4 @@
|
||||
.error {
|
||||
display: block;
|
||||
color: #ba1a1a;
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { TwitchRedemptionDropdownComponent } from './twitch-redemption-dropdown.component';
|
||||
|
||||
describe('TwitchRedemptionDropdownComponent', () => {
|
||||
let component: TwitchRedemptionDropdownComponent;
|
||||
let fixture: ComponentFixture<TwitchRedemptionDropdownComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [TwitchRedemptionDropdownComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(TwitchRedemptionDropdownComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,88 @@
|
||||
import { Component, EventEmitter, inject, input, Input, OnInit, Output } from '@angular/core';
|
||||
import { FormControl, ReactiveFormsModule, Validators } 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';
|
||||
|
||||
@Component({
|
||||
selector: 'twitch-redemption-dropdown',
|
||||
imports: [MatAutocompleteModule, MatFormFieldModule, MatInputModule, ReactiveFormsModule],
|
||||
templateUrl: './twitch-redemption-dropdown.component.html',
|
||||
styleUrl: './twitch-redemption-dropdown.component.scss'
|
||||
})
|
||||
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);
|
||||
|
||||
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);
|
||||
this.formControl.setValue(redemption);
|
||||
}
|
||||
|
||||
get filteredRedemptions() {
|
||||
const value = this.formControl.value;
|
||||
if (typeof (value) == 'string') {
|
||||
return this.twitchRedemptions.filter(r => r.title.toLowerCase().includes(value.toLowerCase()));
|
||||
}
|
||||
return this.twitchRedemptions;
|
||||
}
|
||||
|
||||
select(event: TwitchRedemption) {
|
||||
this.twitchRedemptionIdChange.emit(event.id);
|
||||
}
|
||||
|
||||
input() {
|
||||
if (this.search && typeof this.formControl.value == 'string') {
|
||||
this.twitchRedemptionIdChange.emit(this.formControl.value);
|
||||
}
|
||||
}
|
||||
|
||||
blur() {
|
||||
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];
|
||||
}
|
||||
if (newValue && newValue.id != this.formControl.value) {
|
||||
this.formControl.setValue(newValue);
|
||||
this.twitchRedemptionIdChange.emit(newValue.id);
|
||||
} else if (!newValue)
|
||||
this.twitchRedemptionIdChange.emit(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
displayFn(value: any) {
|
||||
return value?.title;
|
||||
}
|
||||
}
|
5
src/app/shared/models/group.ts
Normal file
5
src/app/shared/models/group.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export default interface Group {
|
||||
id: string;
|
||||
name: string;
|
||||
priority: number;
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
export default interface RedeemableAction {
|
||||
user_id: string
|
||||
name: string
|
||||
type: string
|
||||
data: any
|
||||
user_id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
data: any;
|
||||
}
|
8
src/app/shared/models/redemption.ts
Normal file
8
src/app/shared/models/redemption.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export default interface Redemption {
|
||||
id: string;
|
||||
user_id: string;
|
||||
redemption_id: string;
|
||||
action_name: string
|
||||
order: number;
|
||||
state: boolean;
|
||||
}
|
5
src/app/shared/models/twitch-redemption.ts
Normal file
5
src/app/shared/models/twitch-redemption.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export default interface TwitchRedemption {
|
||||
id: string;
|
||||
broadcaster_id: string;
|
||||
title: string;
|
||||
}
|
12
src/app/shared/validators/integer.ts
Normal file
12
src/app/shared/validators/integer.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { AbstractControl, ValidationErrors } from "@angular/forms";
|
||||
|
||||
|
||||
export function integerValidator(control: AbstractControl): ValidationErrors | null {
|
||||
const value = control.value;
|
||||
|
||||
if (!value)
|
||||
return null;
|
||||
|
||||
const matches = (value | 0) == value;
|
||||
return matches ? null : { integer: true };
|
||||
}
|
14
src/app/shared/validators/of-type.ts
Normal file
14
src/app/shared/validators/of-type.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { AbstractControl, ValidationErrors, ValidatorFn } from "@angular/forms";
|
||||
|
||||
|
||||
export function createTypeValidator(type: string): ValidatorFn {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
const value = control.value;
|
||||
|
||||
if (!value)
|
||||
return null;
|
||||
|
||||
const matches = value.constructor.name === type
|
||||
return matches ? null: { invalidType: 'Invalid choice.' };
|
||||
}
|
||||
}
|
@ -56,7 +56,6 @@ export class TwitchAuthCallbackComponent implements OnInit {
|
||||
localStorage.setItem('jwt', response.token);
|
||||
this.auth.update();
|
||||
|
||||
console.log('logged in. so nav to tts login.');
|
||||
await this.router.navigate(['tts-login']);
|
||||
});
|
||||
}
|
||||
|
@ -2,3 +2,29 @@
|
||||
|
||||
html, body { height: 100%; }
|
||||
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
|
||||
|
||||
.mat-mdc-dialog-surface {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/* width */
|
||||
::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
/* Track */
|
||||
::-webkit-scrollbar-track {
|
||||
box-shadow: inset 0 0 5px grey;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* Handle */
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: darkgrey;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* Handle on hover */
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgb(122, 122, 122);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user