Cleaned up Redemptions with use of AsyncPipe & input transformers.

This commit is contained in:
Tom 2025-04-09 20:57:19 +00:00
parent b0f9a2dea8
commit daa500111c
14 changed files with 136 additions and 203 deletions

View File

@ -1,9 +1,8 @@
import { Component, EventEmitter, inject, input, Input, OnInit, Output } from '@angular/core'; import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms'; import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { ActivatedRoute } from '@angular/router';
import RedeemableAction from '../../shared/models/redeemable-action'; import RedeemableAction from '../../shared/models/redeemable-action';
@Component({ @Component({
@ -21,20 +20,9 @@ export class ActionDropdownComponent implements OnInit {
@Input() action: string | undefined; @Input() action: string | undefined;
@Output() readonly actionChange = new EventEmitter<string>(); @Output() readonly actionChange = new EventEmitter<string>();
private readonly route = inject(ActivatedRoute);
errorMessageKeys: string[] = [] errorMessageKeys: string[] = []
constructor() {
this.route.data.subscribe(data => {
if (!data['redeemableActions'])
return;
this.actions = data['redeemableActions'];
});
}
ngOnInit(): void { ngOnInit(): void {
this.errorMessageKeys = Object.keys(this.errorMessages); this.errorMessageKeys = Object.keys(this.errorMessages);

View File

@ -58,7 +58,7 @@ export class ActionsComponent implements OnInit, OnDestroy {
ngOnInit(): void { ngOnInit(): void {
this.route.data.subscribe(data => this._actions = data['redeemableActions'] || []); this.route.data.subscribe(data => this._actions = data['redeemableActions'] || []);
this.subscriptions.push(this.redeemableActionService.delete$?.subscribe(_ => this.redeemableActionService.fetch().subscribe(a => this._actions = a))); this.subscriptions.push(this.redeemableActionService.changes$?.subscribe(a => this._actions = a));
this.client.fetchRedeemableActions(); this.client.fetchRedeemableActions();
} }

View File

@ -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 Redemption from '../../shared/models/redemption';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card'; 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 RedeemableAction from '../../shared/models/redeemable-action';
import { integerValidator } from '../../shared/validators/integer'; import { integerValidator } from '../../shared/validators/integer';
import { createTypeValidator } from '../../shared/validators/of-type'; import { createTypeValidator } from '../../shared/validators/of-type';
import RedemptionService from '../../shared/services/redemption.service';
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'redemption-item-edit', selector: 'redemption-item-edit',
@ -33,10 +31,9 @@ import { Subscription } from 'rxjs';
styleUrl: './redemption-item-edit.component.scss' styleUrl: './redemption-item-edit.component.scss'
}) })
export class RedemptionItemEditComponent implements OnInit { export class RedemptionItemEditComponent implements OnInit {
readonly client = inject(HermesClientService); private readonly client = inject(HermesClientService);
readonly redemptionService = inject(RedemptionService); private readonly dialogRef = inject(MatDialogRef<RedemptionItemEditComponent>);
readonly dialogRef = inject(MatDialogRef<RedemptionItemEditComponent>); private readonly data = inject<{ redemption: Redemption, twitchRedemptions: { [id: string]: TwitchRedemption }, redeemableActions: RedeemableAction[] }>(MAT_DIALOG_DATA);
readonly data = inject<{ redemption: Redemption, twitchRedemptions: TwitchRedemption[], redeemableActions: RedeemableAction[] }>(MAT_DIALOG_DATA);
redemptionFormControl = new FormControl<TwitchRedemption | string | undefined>(undefined, [Validators.required, createTypeValidator('Object')]); redemptionFormControl = new FormControl<TwitchRedemption | string | undefined>(undefined, [Validators.required, createTypeValidator('Object')]);
redemptionErrorMessages: { [errorKey: string]: string } = { 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 }; redemption: Redemption = { id: '', user_id: '', redemption_id: '', action_name: '', order: 0, state: true };
twitchRedemptions: TwitchRedemption[] = []; twitchRedemptions: { [id: string]: TwitchRedemption } = {};
redeemableActions: RedeemableAction[] = []; redeemableActions: RedeemableAction[] = [];
waitForResponse = false; waitForResponse = false;
responseError: string | undefined = undefined; responseError: string | undefined = undefined;
ngOnInit(): void { ngOnInit(): void {
this.redemption = this.data.redemption; this.redemption = this.data.redemption;
this.orderFormControl.setValue(this.redemption.order);
this.twitchRedemptions = this.data.twitchRedemptions; this.twitchRedemptions = this.data.twitchRedemptions;
this.redeemableActions = this.data.redeemableActions; this.redeemableActions = this.data.redeemableActions;
this.orderFormControl.setValue(this.redemption.order);
this.orderErrorMessageKeys = Object.keys(this.orderErrorMessages); this.orderErrorMessageKeys = Object.keys(this.orderErrorMessages);
} }
@ -84,7 +82,9 @@ export class RedemptionItemEditComponent implements OnInit {
if (this.waitForResponse) if (this.waitForResponse)
return; return;
this.waitForResponse = true this.waitForResponse = true;
this.responseError = undefined;
const id = this.redemption.id; 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) this.client.first((d: any) => d.op == 4 && d.d.request.type == 'delete_redemption' && d.d.request.data.id == id)
?.subscribe({ ?.subscribe({
@ -107,6 +107,7 @@ export class RedemptionItemEditComponent implements OnInit {
this.waitForResponse = true; this.waitForResponse = true;
this.responseError = undefined; this.responseError = undefined;
const order = this.orderFormControl.value; const order = this.orderFormControl.value;
if (order == null) { if (order == null) {
this.responseError = 'Order must be an integer.'; this.responseError = 'Order must be an integer.';
@ -114,16 +115,14 @@ export class RedemptionItemEditComponent implements OnInit {
return; return;
} }
this.waitForResponse = true;
const isNew = !this.redemption.id; const isNew = !this.redemption.id;
if (isNew) { 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({ ?.subscribe({
next: (d) => { next: (d) => {
if (d.d.error) { if (d.d.error) {
this.responseError = d.d.error; this.responseError = d.d.error;
} else { } else {
this.redemption.order = order;
this.dialogRef.close(d.d.data); 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); this.client.createRedemption(this.redemption.redemption_id, this.redemption.action_name, order);
} else { } else {
this.client.first((d: any) => d.op == 4 && d.d.request.type == 'update_redemption' && d.d.data.id == this.redemption.id) this.client.first((d: any) => d.op == 4 && d.d.request.type == 'update_redemption' && d.d.data.id == this.redemption.id)
?.subscribe({ ?.subscribe({
next: (d) => { next: (d) => {
if (d.d.error) { if (d.d.error) {
this.responseError = d.d.error; this.responseError = d.d.error;
} else { } else {
this.redemption.order = order;
this.dialogRef.close(d.d.data); this.dialogRef.close(d.d.data);
} }
}, },

View File

@ -14,10 +14,12 @@
</mat-expansion-panel-header> </mat-expansion-panel-header>
<div class="filters"> <div class="filters">
<twitch-redemption-dropdown [(twitchRedemptionId)]="filter_redemption" <twitch-redemption-dropdown [twitchRedemptions]="twitchRedemptions()"
[(twitchRedemptionId)]="filter_redemption"
[search]="true" /> [search]="true" />
<action-dropdown [(action)]="filter_action_name" <action-dropdown [search]="true"
[search]="true" /> [actions]="actions()"
[(action)]="filter_action_name" />
</div> </div>
</mat-expansion-panel> </mat-expansion-panel>

View File

@ -1,23 +1,19 @@
import { Component, inject, OnDestroy, signal } from '@angular/core'; import { Component, inject, input, signal } from '@angular/core';
import RedemptionService from '../../shared/services/redemption.service';
import Redemption from '../../shared/models/redemption'; import Redemption from '../../shared/models/redemption';
import { MatCardModule } from '@angular/material/card'; import { MatCardModule } from '@angular/material/card';
import { TwitchRedemptionDropdownComponent } from "../twitch-redemption-dropdown/twitch-redemption-dropdown.component"; import { TwitchRedemptionDropdownComponent } from "../twitch-redemption-dropdown/twitch-redemption-dropdown.component";
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { ActivatedRoute } from '@angular/router';
import { ActionDropdownComponent } from '../../actions/action-dropdown/action-dropdown.component'; import { ActionDropdownComponent } from '../../actions/action-dropdown/action-dropdown.component';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
import TwitchRedemption from '../../shared/models/twitch-redemption';
import { RedemptionItemEditComponent } from '../redemption-item-edit/redemption-item-edit.component'; import { RedemptionItemEditComponent } from '../redemption-item-edit/redemption-item-edit.component';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import RedeemableAction from '../../shared/models/redeemable-action'; import RedeemableAction from '../../shared/models/redeemable-action';
import { MatExpansionModule } from '@angular/material/expansion'; import { MatExpansionModule } from '@angular/material/expansion';
import { ScrollingModule } from '@angular/cdk/scrolling'; import { ScrollingModule } from '@angular/cdk/scrolling';
import { Subscription } from 'rxjs'; import { toTwitchRedemptionDict } from '../../shared/transformers/twitch-redemption.transformer';
import { HermesClientService } from '../../hermes-client.service';
@Component({ @Component({
selector: 'redemption-list', selector: 'redemption-list',
@ -36,97 +32,45 @@ import { HermesClientService } from '../../hermes-client.service';
templateUrl: './redemption-list.component.html', templateUrl: './redemption-list.component.html',
styleUrl: './redemption-list.component.scss' styleUrl: './redemption-list.component.scss'
}) })
export class RedemptionListComponent implements OnDestroy { export class RedemptionListComponent {
private readonly client = inject(HermesClientService);
private readonly redemptionService = inject(RedemptionService);
private readonly route = inject(ActivatedRoute);
readonly dialog = inject(MatDialog); 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']; displayedColumns: string[] = ['twitch-redemption', 'action-name', 'order', 'misc'];
filter_redemption: string | undefined; filter_redemption: string | undefined;
filter_action_name: string | undefined; filter_action_name: string | undefined;
readonly panelOpenState = signal(false); readonly panelOpenState = signal(false);
private _subscriptions: Subscription[] = []
_redemptions = input.required<Redemption[]>({ alias: 'redemptions' });
twitchRedemptions = input.required({
alias: 'twitchRedemptions',
transform: toTwitchRedemptionDict,
});
//twitchRedemptions = computed<{ [id: string]: string } | null>(() => ({}));
actions = input.required<RedeemableAction[]>();
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() {
this.panelOpenState.set(false);
}
ngOnDestroy(): void {
this._subscriptions.forEach(s => s.unsubscribe());
}
compare(a: Redemption, b: Redemption) { 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() { get redemptions() {
const redemptionFilter = this.filter_redemption?.toString().toLowerCase();
const actionFilter = this.filter_action_name?.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();
filtered = filtered.filter(r => !actionFilter || r.action_name.toLowerCase().includes(actionFilter)); 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; return filtered;
} }
getTwitchRedemptionNameById(id: string) { getTwitchRedemptionNameById(id: string) {
return this._twitchRedemptionsDict[id]; return this.twitchRedemptions()![id]?.title;
} }
add(): void { add(): void {
@ -134,39 +78,9 @@ export class RedemptionListComponent implements OnDestroy {
} }
openDialog(redemption: Redemption): void { openDialog(redemption: Redemption): void {
const dialogRef = this.dialog.open(RedemptionItemEditComponent, { this.dialog.open(RedemptionItemEditComponent, {
data: { redemption: { ...redemption }, twitchRedemptions: this._twitchRedemptions, redeemableActions: this._actions }, data: { redemption: { ...redemption }, twitchRedemptions: this.twitchRedemptions(), redeemableActions: this.actions() },
maxWidth: '100vw' 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);
}
}
});
} }
} }

View File

@ -1,5 +1,7 @@
<div class="root"> <div class="root">
<div class="content"> <div class="content">
<redemption-list /> <redemption-list [redemptions]="redemptions$ | async"
[twitchRedemptions]="twitchRedemptions$ | async"
[actions]="actions$ | async" />
</div> </div>
</div> </div>

View File

@ -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 { 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 RedemptionService from '../../shared/services/redemption.service';
import { Observable, of } from 'rxjs'; import { AsyncPipe } from '@angular/common';
import Redemption from '../../shared/models/redemption'; import TwitchRedemptionService from '../../shared/services/twitch-redemption.service';
import { ActivatedRoute } from '@angular/router'; import RedeemableActionService from '../../shared/services/redeemable-action.service';
@Component({ @Component({
selector: 'redemptions', selector: 'redemptions',
imports: [RedemptionListComponent], imports: [
AsyncPipe,
RedemptionListComponent
],
templateUrl: './redemptions.component.html', templateUrl: './redemptions.component.html',
styleUrl: './redemptions.component.scss' styleUrl: './redemptions.component.scss'
}) })
export class RedemptionsComponent implements OnInit { export class RedemptionsComponent {
client = inject(HermesClientService); private readonly twitchRedemptionService = inject(TwitchRedemptionService);
http = inject(HttpClient); private readonly redemptionService = inject(RedemptionService);
route = inject(ActivatedRoute); private readonly actionService = inject(RedeemableActionService);
redemptionService = inject(RedemptionService);
redemptions: Observable<Redemption[]> | undefined;
ngOnInit(): void { redemptions$ = this.redemptionService.changes$;
twitchRedemptions$ = this.twitchRedemptionService.fetch();
} actions$ = this.actionService.changes$;
} }

View File

@ -11,7 +11,7 @@
<mat-autocomplete #auto="matAutocomplete" <mat-autocomplete #auto="matAutocomplete"
[displayWith]="displayFn" [displayWith]="displayFn"
(optionSelected)="select($event.option.value)"> (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-option [value]="redemption">{{redemption.title}}</mat-option>
} }
</mat-autocomplete> </mat-autocomplete>

View File

@ -1,10 +1,10 @@
import { Component, EventEmitter, inject, input, Input, OnInit, Output } from '@angular/core'; import { Component, input, Input, model, OnInit } from '@angular/core';
import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms'; import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import TwitchRedemption from '../../shared/models/twitch-redemption'; import TwitchRedemption from '../../shared/models/twitch-redemption';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { ActivatedRoute } from '@angular/router'; import { toTwitchRedemptionArray } from '../../shared/transformers/twitch-redemption.transformer';
@Component({ @Component({
selector: 'twitch-redemption-dropdown', selector: 'twitch-redemption-dropdown',
@ -16,69 +16,60 @@ export class TwitchRedemptionDropdownComponent implements OnInit {
@Input() formControl = new FormControl<TwitchRedemption | string | undefined>(undefined); @Input() formControl = new FormControl<TwitchRedemption | string | undefined>(undefined);
@Input() errorMessages: { [errorKey: string]: string } = {}; @Input() errorMessages: { [errorKey: string]: string } = {};
@Input() search: boolean = false; twitchRedemptions = input.required({
@Input() twitchRedemptions: TwitchRedemption[] = []; transform: toTwitchRedemptionArray
@Input() twitchRedemptionId: string | undefined; });
@Output() readonly twitchRedemptionIdChange = new EventEmitter<string>(); twitchRedemptionId = model<string | undefined>();
search = input<boolean>(false);
private readonly route = inject(ActivatedRoute);
errorMessageKeys: string[] = []; errorMessageKeys: string[] = [];
constructor() {
this.route.data.subscribe(data => {
if (!data['twitchRedemptions'])
return;
this.twitchRedemptions = data['twitchRedemptions'];
});
}
ngOnInit(): void { ngOnInit(): void {
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); 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() { get filteredRedemptions() {
const value = this.formControl.value; const value = this.formControl.value;
if (typeof (value) == 'string') { if (this.twitchRedemptions() && typeof (value) == 'string') {
return this.twitchRedemptions.filter(r => r.title.toLowerCase().includes(value.toLowerCase())); return this.twitchRedemptions()!.filter(r => r.title.toLowerCase().includes(value.toLowerCase()));
} }
return this.twitchRedemptions; return this.twitchRedemptions();
} }
select(event: TwitchRedemption) { select(event: TwitchRedemption) {
this.twitchRedemptionIdChange.emit(event.id); this.twitchRedemptionId.set(event.id);
} }
input() { input() {
if (this.search && typeof this.formControl.value == 'string') { if (this.search() && typeof this.formControl.value == 'string') {
this.twitchRedemptionIdChange.emit(this.formControl.value); this.twitchRedemptionId.set(this.formControl.value);
} }
} }
blur() { blur() {
if (this.filteredRedemptions == null)
return;
if (!this.search && typeof this.formControl.value == 'string') { if (!this.search && typeof this.formControl.value == 'string') {
const name = this.formControl.value; const name = this.formControl.value;
const nameLower = name.toLowerCase(); const nameLower = name.toLowerCase();
let newValue: TwitchRedemption | undefined = undefined; let newValue: TwitchRedemption | undefined;
const insenstiveActions = this.filteredRedemptions.filter(a => a.title.toLowerCase() == nameLower); const filtered = this.filteredRedemptions?.filter(a => a.title.toLowerCase() == nameLower);
if (insenstiveActions.length > 1) { if (filtered.length > 1) {
const sensitiveAction = insenstiveActions.find(a => a.title == name); newValue = filtered.find(a => a.title == name);
newValue = sensitiveAction ?? undefined; } else if (filtered.length == 1) {
} else if (insenstiveActions.length == 1) { newValue = filtered[0];
newValue = insenstiveActions[0];
} }
if (newValue && newValue.id != this.formControl.value) { if (newValue && newValue.id != this.formControl.value) {
this.formControl.setValue(newValue); this.formControl.setValue(newValue);
this.twitchRedemptionIdChange.emit(newValue.id); this.twitchRedemptionId.set(newValue.id);
} else if (!newValue) } else if (!newValue)
this.twitchRedemptionIdChange.emit(undefined); this.twitchRedemptionId.set(undefined);
} }
} }

View File

@ -1,6 +1,6 @@
import { inject, Injectable } from '@angular/core'; import { inject, Injectable } from '@angular/core';
import { HermesClientService } from '../../hermes-client.service'; 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 RedeemableAction from '../models/redeemable-action';
import EventService from './EventService'; import EventService from './EventService';
@ -12,6 +12,7 @@ export default class RedeemableActionService {
private readonly events = inject(EventService); private readonly events = inject(EventService);
private data: RedeemableAction[] = []; private data: RedeemableAction[] = [];
private loaded = false; private loaded = false;
changes$: Observable<any>;
create$: Observable<any> | undefined; create$: Observable<any> | undefined;
update$: Observable<any> | undefined; update$: Observable<any> | undefined;
delete$: Observable<any> | undefined; delete$: Observable<any> | undefined;
@ -21,6 +22,11 @@ export default class RedeemableActionService {
this.create$ = this.client.filterByRequestType('create_redeemable_action'); this.create$ = this.client.filterByRequestType('create_redeemable_action');
this.update$ = this.client.filterByRequestType('update_redeemable_action'); this.update$ = this.client.filterByRequestType('update_redeemable_action');
this.delete$ = this.client.filterByRequestType('delete_redeemable_action'); this.delete$ = this.client.filterByRequestType('delete_redeemable_action');
this.changes$ = merge<any>(this.create$, this.update$, this.delete$)
.pipe(
startWith(null),
map(d => this.data.slice()),
);
this.create$?.subscribe(d => this.data.push(d.data)); this.create$?.subscribe(d => this.data.push(d.data));
this.update$?.subscribe(d => { this.update$?.subscribe(d => {

View File

@ -1,7 +1,7 @@
import { inject, Injectable } from '@angular/core'; import { inject, Injectable } from '@angular/core';
import Redemption from '../models/redemption'; import Redemption from '../models/redemption';
import { HermesClientService } from '../../hermes-client.service'; import { HermesClientService } from '../../hermes-client.service';
import { map, Observable, of } from 'rxjs'; import { map, merge, Observable, of, startWith } from 'rxjs';
import EventService from './EventService'; import EventService from './EventService';
@Injectable({ @Injectable({
@ -12,6 +12,7 @@ export default class RedemptionService {
private readonly events = inject(EventService); private readonly events = inject(EventService);
private data: Redemption[] = [] private data: Redemption[] = []
private loaded = false; private loaded = false;
changes$: Observable<any>;
create$: Observable<any> | undefined; create$: Observable<any> | undefined;
update$: Observable<any> | undefined; update$: Observable<any> | undefined;
delete$: Observable<any> | undefined; delete$: Observable<any> | undefined;
@ -20,6 +21,11 @@ export default class RedemptionService {
this.create$ = this.client.filterByRequestType('create_redemption'); this.create$ = this.client.filterByRequestType('create_redemption');
this.update$ = this.client.filterByRequestType('update_redemption'); this.update$ = this.client.filterByRequestType('update_redemption');
this.delete$ = this.client.filterByRequestType('delete_redemption'); this.delete$ = this.client.filterByRequestType('delete_redemption');
this.changes$ = merge<any>(this.create$, this.update$, this.delete$)
.pipe(
startWith(null),
map(d => this.data.slice()),
);
this.create$?.subscribe(d => this.data.push(d.data)); this.create$?.subscribe(d => this.data.push(d.data));
this.update$?.subscribe(d => { this.update$?.subscribe(d => {

View File

@ -2,7 +2,7 @@ import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core'; import { inject, Injectable } from '@angular/core';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
import TwitchRedemption from '../models/twitch-redemption'; import TwitchRedemption from '../models/twitch-redemption';
import { catchError, EMPTY, Observable, of } from 'rxjs'; import { catchError, EMPTY, of } from 'rxjs';
import EventService from './EventService'; import EventService from './EventService';
@Injectable({ @Injectable({
@ -11,6 +11,7 @@ import EventService from './EventService';
export default class TwitchRedemptionService { export default class TwitchRedemptionService {
private readonly http = inject(HttpClient); private readonly http = inject(HttpClient);
private readonly events = inject(EventService); private readonly events = inject(EventService);
private twitchRedemptions: TwitchRedemption[] = []; private twitchRedemptions: TwitchRedemption[] = [];
private loaded = false; private loaded = false;

View 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);
}

View 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) })))
}