Skip to content

Commit

Permalink
v3.9.3
Browse files Browse the repository at this point in the history
- For missing toc.json/settings.json (#123)
- Resolved an issue that could cause the player presence widget to fail to display for non-sync-start games.
- Gameboard now only looks for settings.json in production environments (or any env that has the settingsJson property of the environment object defined)
  • Loading branch information
sei-bstein committed Jun 1, 2023
1 parent e7832f2 commit a721d36
Show file tree
Hide file tree
Showing 12 changed files with 108 additions and 76 deletions.
47 changes: 31 additions & 16 deletions projects/gameboard-ui/src/app/api/toc.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,22 @@

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, of, BehaviorSubject, iif, Subject } from 'rxjs';
import { switchMap, map, tap, delay } from 'rxjs/operators';
import { Observable, of, BehaviorSubject } from 'rxjs';
import { switchMap, map, tap, delay, catchError } from 'rxjs/operators';
import { ConfigService } from '../utility/config.service';
import { LogService } from '../services/log.service';

@Injectable({
providedIn: 'root'
})
@Injectable({ providedIn: 'root' })
export class TocService {
toc$: Observable<TocFile[]>;
tocfile$: (id: string) => Observable<string>;
loaded$ = new BehaviorSubject<boolean>(false);
private cache: TocFile[] = [];

constructor(
config: ConfigService,
private http: HttpClient,
private config: ConfigService
private log: LogService
) {
const tag = `?t=${new Date().valueOf()}`;
const tocUrl = `${config.tochost}/${config.settings.tocfile + ''}${tag}`;
Expand All @@ -30,32 +30,47 @@ export class TocService {
url += `/${config.settings.tocfile?.substring(0, i)}`;
}

this.toc$ = iif(
() => !!config.settings.tocfile,
http.get<string[]>(tocUrl).pipe(
if (!!config.settings.tocfile) {
this.toc$ = http.get<string[]>(tocUrl).pipe(
catchError(err => {
// don't report error here - ops will know what the 404 means
this.cache = [];
return [];
}),
switchMap((list) => this.mapTocFromList(list)),
tap(list => this.cache = list),
),
of([])
).pipe(
tap(() => this.loaded$.next(true))
);
);
}
else {
this.log.logInfo("No toc file configured. Skipped loading.");
this.toc$ = of([]);
}

this.toc$ = this.toc$.pipe(tap(toc => this.loaded$.next(true)));

this.tocfile$ = (id: string) => {
const tocfile = this.cache.find(f =>
f.filename === id ||
f.link === id
);

if (!tocfile) {
return of('not found');
}

if (!!tocfile.text) {
return of(tocfile.text);
}

const tocFileUrl = `${url}/${tocfile?.filename}${tag}`;
return this.http.get(
`${url}/${tocfile?.filename}${tag}`,
{ responseType: 'text'}
tocFileUrl,
{ responseType: 'text' }
).pipe(
catchError(err => {
// don't report - ops will know 404
return '';
}),
tap(t => tocfile.text = t)
);
};
Expand Down
4 changes: 2 additions & 2 deletions projects/gameboard-ui/src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
<nav [class]="custom_bg" [class.nonsticky]="!(layoutService.stickyMenu$ | async)">
<div class="nav-items d-flex align-items-center justify-content-end">
<a class="btn btn-link text-success mx-1" routerLinkActive="text-info" [routerLink]="['/home']">Home</a>
<ng-container *ngFor="let t of toc$ | async">
<a class="btn btn-link text-success mx-1" routerLinkActive="text-info"
<ng-container *ngIf="(toc$ | async)?.length">
<a *ngFor="let t of toc$ | async" class="btn btn-link text-success mx-1" routerLinkActive="text-info"
[routerLink]="['doc', t.link]">{{t.display}}</a>
</ng-container>
<a *ngIf="isPracticeModeEnabled" class="btn btn-link text-success mx-1" routerLinkActive="text-info"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@ export class GamespaceQuizComponent {
errors: Error[] = [];
faSubmit = faCloudUploadAlt;

constructor(
private api: BoardService
) { }
constructor(private api: BoardService) { }

submit(): void {
this.pending = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

<ng-container *ngIf="ctx$ | async as ctx">
<div *ngIf="ctx.player" class="team-leader d-flex align-items-center align-content-center">
<app-player-avatar-list *ngIf="ctx.hasTeammates" [avatarUris]="avatarUris" [captainSession]="ctx.player.session"
<app-player-avatar-list *ngIf="ctx.hasTeammates" [avatarUris]="ctx.avatarUris" [captainSession]="ctx.player.session"
class="d-inline-block mr-4 my-2"></app-player-avatar-list>
<app-player-avatar *ngIf="!ctx.hasTeammates" [avatarUri]="ctx.player.sponsorLogo" [enableSessionStatus]="true"
[session]="ctx.player.session" class="d-inline-block mr-4 my-2"></app-player-avatar>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
// Copyright 2021 Carnegie Mellon University. All Rights Reserved.
// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information.

import { Component, Input, OnInit, Output, EventEmitter } from '@angular/core';
import { BehaviorSubject, combineLatest, Observable, Subscription } from 'rxjs';
import { first, map, tap } from 'rxjs/operators';
import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
import { combineLatest, Observable, of } from 'rxjs';
import { first, map, startWith, tap } from 'rxjs/operators';
import { faChevronCircleUp } from '@fortawesome/free-solid-svg-icons';
import { HubPlayer, Player } from '../../api/player-models';
import { PlayerService } from '../../api/player.service';
import { NotificationService } from '../../services/notification.service';
import { GameHubService } from '../../services/signalR/game-hub.service';
import { SyncStartState } from '../game.models';
import { SyncStartService } from '../../services/sync-start.service';
import { SimpleEntity } from '../../api/models';
import { HubConnectionState } from '@microsoft/signalr';
import { LogService } from '../../services/log.service';

interface PlayerPresenceContext {
avatarUris: string[];
hasTeammates: boolean;
manager: HubPlayer | undefined;
allPlayers: HubPlayer[];
player: HubPlayer;
playerIsManager: boolean;
readyPlayers: SimpleEntity[];
notReadyPlayers: SimpleEntity[];
teamAvatar: string[],
teamName: string;
}
Expand All @@ -31,62 +31,67 @@ interface PlayerPresenceContext {
templateUrl: './player-presence.component.html',
})
export class PlayerPresenceComponent implements OnInit {
@Input() player$?: Observable<Player | undefined>;
@Input() player$: Observable<Player | undefined> = of(undefined);
@Input() isSyncStartGame: boolean = false;
@Output() onManagerPromoted = new EventEmitter<string>();

private syncStartState$ = new BehaviorSubject<SyncStartState | null>(null);
private syncStartStateSubscription?: Subscription;

protected promoteIcon = faChevronCircleUp;
protected ctx$?: Observable<PlayerPresenceContext | null>;
protected avatarUris: string[] = [];

constructor(
private gameHub: GameHubService,
private hub: NotificationService,
private log: LogService,
private playerApi: PlayerService,
private syncStartService: SyncStartService,
) { }

ngOnInit(): void {
this.ctx$ = combineLatest([
this.hub.state$,
this.hub.actors$,
this.player$,
this.gameHub.syncStartChanged$
this.gameHub.syncStartChanged$.pipe(startWith(null))
]).pipe(
map(combo => combo as unknown as { 0: HubPlayer[], 1: HubPlayer, 2: SyncStartState }),
map(combo => ({ actors: combo[0], player: combo[1], syncStartState: combo[2] })),
// map(combo => combo as unknown as { 0: HubPlayer[], 1: HubPlayer, 2: SyncStartState }),
map(combo => ({ hubState: combo[0], actors: combo[1], player: combo[2], syncStartState: combo[3] })),
map(context => {
if (!context.player) {
if (!context.hubState || context.hubState.connectionState == HubConnectionState.Disconnected) {
this.log.logWarning("Can't render player presence component: SignalR hub is disconnected.");
return null;
}

const actorInfo = this.findPlayerAndTeammates(context.player, context.actors);
this.avatarUris = actorInfo.allPlayers.map(p => p.sponsorLogo);

// grab team members on this screen and show their ready/not ready flag
// (read it into the player objects coming from the hub - should think about streamlining this later)
const playerReadyStates = this.syncStartService.getAllPlayers(context.syncStartState);
for (let player of actorInfo.allPlayers) {
const playerReadyState = playerReadyStates.find(p => p.id === player.id);

if (playerReadyState) {
player.isReady = playerReadyState.isReady;
}
if (!context.player) {
this.log.logWarning("Can't render player presence component: the context has no Player object.");
return null;
}

return {
const actorInfo = this.findPlayerAndTeammates(context.player, context.actors);
const ctx: PlayerPresenceContext = {
avatarUris: actorInfo.allPlayers.map(p => p.sponsorLogo),
hasTeammates: !!actorInfo.teammates.length,
manager: actorInfo.manager,
allPlayers: actorInfo.allPlayers,
player: actorInfo.player,
playerIsManager: !!actorInfo.manager && actorInfo.manager.id === actorInfo.player?.id,
readyPlayers: this.syncStartService.getAllPlayers(context.syncStartState),
notReadyPlayers: this.syncStartService.getNotReadyPlayers(context.syncStartState),
teamAvatar: this.computeTeamAvatarList(actorInfo.allPlayers),
teamName: actorInfo.manager?.approvedName || actorInfo.player?.approvedName || "",
};

if (this.isSyncStartGame && context.syncStartState) {
// grab team members on this screen and show their ready/not ready flag
// (read it into the player objects coming from the hub - should think about streamlining this later)
const playerReadyStates = this.syncStartService.getAllPlayers(context.syncStartState);
for (let player of actorInfo.allPlayers) {
const playerReadyState = playerReadyStates.find(p => p.id === player.id);

if (playerReadyState) {
player.isReady = playerReadyState.isReady;
}
}
}

return ctx;
})
);
}
Expand Down Expand Up @@ -116,6 +121,7 @@ export class PlayerPresenceComponent implements OnInit {
if (!s.id) {
throw new Error("Can't promote a manager while the hub is disconnected.");
}

if (!localPlayer) {
throw new Error("Can't resolve the current player to promote manager.");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@
</ng-container>
</div>

<app-player-presence *ngIf="player$ | async" [player$]=" player$"
[isSyncStartGame]="ctx.game.requireSynchronizedStart"></app-player-presence>
<ng-container *ngIf="playerObservable$ && (playerObservable$ | async)">
<app-player-presence [player$]="playerObservable$"
[isSyncStartGame]="ctx.game.requireSynchronizedStart"></app-player-presence>
</ng-container>

<app-gameboard-performance-summary *ngIf="performanceSummaryViewModel$ | async"
[ctx$]="performanceSummaryViewModel$"></app-gameboard-performance-summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import { Component, EventEmitter, Input, OnDestroy, Output } from '@angular/core';
import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
import { BehaviorSubject, combineLatest, interval, Observable, of, Subject, Subscription } from 'rxjs';
import { BehaviorSubject, firstValueFrom, Observable, Subscription } from 'rxjs';
import { first, tap } from 'rxjs/operators';
import { GameContext } from '../../api/models';
import { Player } from '../../api/player-models';
Expand All @@ -25,6 +25,8 @@ export class PlayerSessionComponent implements OnDestroy {

errors: any[] = [];
myCtx$!: Observable<GameContext | undefined>;
player$ = new BehaviorSubject<Player | undefined>(undefined);
playerObservable$ = this.player$.asObservable();

private ctxSub?: Subscription;

Expand All @@ -33,7 +35,6 @@ export class PlayerSessionComponent implements OnDestroy {
protected modalConfig?: ModalConfirmConfig;
protected isDoubleChecking = false;
protected performanceSummaryViewModel$ = new BehaviorSubject<GameboardPerformanceSummaryViewModel | undefined>(undefined);
protected player$ = new BehaviorSubject<Player | undefined>(undefined);

constructor(
private api: PlayerService,
Expand Down Expand Up @@ -91,13 +92,9 @@ export class PlayerSessionComponent implements OnDestroy {
this.isDoubleChecking = isDoubleChecking;
}

handleStart(player: Player): void {
this.api.start(player).pipe(
first()
).subscribe(
p => this.onSessionStart.emit(p),
err => this.errors.push(err),
);
async handleStart(player: Player): Promise<void> {
const startedPlayer = await firstValueFrom(this.api.start(player));
this.onSessionStart.emit(startedPlayer);
}

handleReset(p: Player): void {
Expand Down
6 changes: 3 additions & 3 deletions projects/gameboard-ui/src/app/services/log.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,17 @@ export class LogService {

logInfo(...params: any[]) {
if (this._logLevel == LogLevel.Info)
console.info(params);
console.info(...params);
}

logWarning(...params: any[]) {
if (this._logLevel <= LogLevel.Warning)
console.warn(params);
console.warn(...params);
}

logError(...params: any[]) {
if (this._logLevel <= LogLevel.Error) {
console.error(params);
console.error(...params);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Component, Injectable, OnDestroy } from '@angular/core';
import { Injectable, OnDestroy } from '@angular/core';
import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
import { Subscription } from 'rxjs';
import { ManageManualChallengeBonusesComponent } from '../admin/components/manage-manual-challenge-bonuses/manage-manual-challenge-bonuses.component';
import { ModalConfirmComponent } from '../core/components/modal/modal-confirm.component';
import { ModalConfirmConfig } from '../core/directives/modal-confirm.directive';

Expand All @@ -25,7 +24,7 @@ export class ModalConfirmService implements OnDestroy {
this.hiddenSub = this.bsModalRef.onHidden?.subscribe(s => this.onHidden(config.onCancel));
}
}

hide(isCancelEvent = false): void {
if (!isCancelEvent) {
this.hiddenSub?.unsubscribe();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import linkifyHtml from 'linkify-html';
import { ClipboardService } from "../../utility/services/clipboard.service";
import { ToastService } from '../../utility/services/toast.service';
import { FontAwesomeService } from '../../services/font-awesome.service';
import { LogService } from '../../services/log.service';

@Component({
selector: 'app-ticket-details',
Expand Down Expand Up @@ -85,6 +86,7 @@ export class TicketDetailsComponent implements AfterViewInit, OnDestroy {
private api: SupportService,
private clipboard: ClipboardService,
private faService: FontAwesomeService,
private logService: LogService,
private playerApi: PlayerService,
private userApi: UserService,
private sanitizer: DomSanitizer,
Expand Down Expand Up @@ -180,8 +182,7 @@ export class TicketDetailsComponent implements AfterViewInit, OnDestroy {
else this.attachmentObjectUrls[imgId] = this.sanitizer.bypassSecurityTrustResourceUrl(url);
}, async (error) => {
// In case of an error, print it
console.log("Error encountered while retrieving image.");
console.log(error);
this.logService.logError("Error encountered while retrieving image:", error);
}
);
}
Expand Down
Loading

0 comments on commit a721d36

Please sign in to comment.