From 7f7b6187cc3bbdd3fe0e944e2661583036e7933b Mon Sep 17 00:00:00 2001 From: Ben Stein Date: Fri, 24 May 2024 12:33:41 -0400 Subject: [PATCH 01/10] Add validation to game editor. --- .../game-editor/game-editor.component.html | 36 +++++++++++++++++-- .../game-editor/game-editor.component.ts | 18 ++++++++-- 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/projects/gameboard-ui/src/app/admin/game-editor/game-editor.component.html b/projects/gameboard-ui/src/app/admin/game-editor/game-editor.component.html index a9cc5ec3..b072ac36 100644 --- a/projects/gameboard-ui/src/app/admin/game-editor/game-editor.component.html +++ b/projects/gameboard-ui/src/app/admin/game-editor/game-editor.component.html @@ -297,12 +297,17 @@

Execution

- + yyyy-mm-ddT00:00:00+00:00
+
+ The game's open date must be less than its close date. +
+
Registration yyyy-mm-ddT00:00:00+00:00 +
@@ -425,10 +431,20 @@

Registration

[(ngModel)]="game.registrationClose"> yyyy-mm-ddT00:00:00+00:00
+ + +
+
+ + The registration period's open date must be prior to its close date. + +
+
+
-
@@ -436,12 +452,26 @@

Registration

+ [min]="game.minTeamSize" [(ngModel)]="game.maxTeamSize">
+
+
+ + The minimum team size must be less than (or equal to) the maximum team size. + +
+ +
+ + The minimum team size must be a positive integer. + +
+
+
diff --git a/projects/gameboard-ui/src/app/admin/game-editor/game-editor.component.ts b/projects/gameboard-ui/src/app/admin/game-editor/game-editor.component.ts index 3d0140ec..824adf6f 100644 --- a/projects/gameboard-ui/src/app/admin/game-editor/game-editor.component.ts +++ b/projects/gameboard-ui/src/app/admin/game-editor/game-editor.component.ts @@ -6,12 +6,11 @@ import { FormGroup, NgForm } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; import { Observable, firstValueFrom } from 'rxjs'; import { debounceTime, filter, map, switchMap, tap } from 'rxjs/operators'; -import { ExternalGameHost, Game, GameEngineMode } from '../../api/game-models'; +import { ExternalGameHost, Game, GameEngineMode, GameRegistrationType } from '../../api/game-models'; import { GameService } from '../../api/game.service'; import { KeyValue } from '@angular/common'; import { AppTitleService } from '@/services/app-title.service'; import { fa } from '@/services/font-awesome.service'; -import { PlayerMode } from '@/api/player-models'; import { ToastService } from '@/utility/services/toast.service'; import { PracticeService } from '@/services/practice.service'; import { FeedbackTemplate } from '@/api/feedback-models'; @@ -81,7 +80,7 @@ export class GameEditorComponent implements AfterViewInit { ngAfterViewInit(): void { this.updated$ = this.form.valueChanges.pipe( - filter(f => !this.form.pristine && (this.form.valid || false)), + filter(f => !this.form.pristine && (this.form.valid || false) && this.doAdditionalValidation(f)), tap(values => { this.dirty = true; this.needsPracticeModeEnabledRefresh = values.playerMode !== this.game.playerMode; @@ -181,4 +180,17 @@ export class GameEditorComponent implements AfterViewInit { if (a.key > b.key) return 1; return 0; } + + private doAdditionalValidation(game: Game) { + if (game.minTeamSize > game.maxTeamSize) + return false; + + if (game.gameStart > game.gameEnd) + return false; + + if (game.registrationType == GameRegistrationType.open && game.registrationOpen > game.registrationClose) + return false; + + return true; + } } From 574005f20f96c7af2b3ef1314113a5373d31d25f Mon Sep 17 00:00:00 2001 From: Ben Stein Date: Tue, 28 May 2024 11:24:21 -0400 Subject: [PATCH 02/10] Don't serve host API key in external host responses. --- .../external-host-editor.component.html | 3 ++- .../external-host-editor/external-host-editor.component.ts | 6 +++--- projects/gameboard-ui/src/app/api/game-models.ts | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/projects/gameboard-ui/src/app/admin/components/external-host-editor/external-host-editor.component.html b/projects/gameboard-ui/src/app/admin/components/external-host-editor/external-host-editor.component.html index 3c04a3ff..53da5174 100644 --- a/projects/gameboard-ui/src/app/admin/components/external-host-editor/external-host-editor.component.html +++ b/projects/gameboard-ui/src/app/admin/components/external-host-editor/external-host-editor.component.html @@ -15,7 +15,8 @@
+ [(ngModel)]="editHost.hostApiKey" + [placeholder]="hasApiKey ? '(key configured - enter a new key here to change it)' : '(e.g. 123ThisIsMyKey!)'">
diff --git a/projects/gameboard-ui/src/app/admin/components/external-host-editor/external-host-editor.component.ts b/projects/gameboard-ui/src/app/admin/components/external-host-editor/external-host-editor.component.ts index 93db6eda..116daa93 100644 --- a/projects/gameboard-ui/src/app/admin/components/external-host-editor/external-host-editor.component.ts +++ b/projects/gameboard-ui/src/app/admin/components/external-host-editor/external-host-editor.component.ts @@ -15,15 +15,14 @@ export class ExternalHostEditorComponent implements OnInit { startupEndpoint: "" }; protected errors: any[] = []; + protected hasApiKey = false; public hostId?: string; public onSave?: (host: UpsertExternalGameHost) => void | Promise; protected subtitle?: string; protected title = "New External Game Host"; protected tryPingResult?: { success: boolean; response?: string }; - constructor( - private externalGameService: ExternalGameService, - ) { } + constructor(private externalGameService: ExternalGameService) { } async ngOnInit() { if (this.hostId) { @@ -34,6 +33,7 @@ export class ExternalHostEditorComponent implements OnInit { throw new Error(`Couldn't resolve host ${this.hostId}.`); this.editHost = host; + this.hasApiKey = host.hasApiKey; this.subtitle = "Edit External Game Host"; this.title = host.name; } diff --git a/projects/gameboard-ui/src/app/api/game-models.ts b/projects/gameboard-ui/src/app/api/game-models.ts index 8594e58b..925dcc09 100644 --- a/projects/gameboard-ui/src/app/api/game-models.ts +++ b/projects/gameboard-ui/src/app/api/game-models.ts @@ -146,7 +146,7 @@ export interface ExternalGameHost { clientUrl: string; destroyResourcesOnDeployFailure?: boolean; gamespaceDeployBatchSize?: number; - hostApiKey?: string; + hasApiKey: boolean; hostUrl: string; pingEndpoint?: string; startupEndpoint: string; From 5d694a89b21c8d422a6dcd6b3086fc3366ce3d50 Mon Sep 17 00:00:00 2001 From: Ben Stein Date: Wed, 29 May 2024 16:29:53 -0400 Subject: [PATCH 03/10] Initial work on game center --- .../src/app/admin/admin.module.ts | 18 +++++- .../game-center/game-center.component.html | 57 +++++++++++++++++++ .../game-center/game-center.component.scss | 3 + .../game-center/game-center.component.ts | 37 ++++++++++++ .../admin/dashboard/dashboard.component.html | 4 ++ .../game-mapper/game-mapper.component.ts | 10 +++- .../game-classification-to-string.pipe.ts | 25 ++++++++ .../gameboard-ui/src/app/api/admin.models.ts | 22 +++++++ .../gameboard-ui/src/app/api/admin.service.ts | 13 ++++- .../src/app/app-routing.module.ts | 1 + .../game-card-image.component.ts | 2 +- .../gameboard-ui/src/app/game/game.module.ts | 12 +--- ...coreboard-team-detail-modal.component.html | 0 ...coreboard-team-detail-modal.component.scss | 0 .../scoreboard-team-detail-modal.component.ts | 0 .../scoreboard/scoreboard.component.html | 0 .../scoreboard/scoreboard.component.scss | 0 .../scoreboard/scoreboard.component.ts | 5 +- .../pipes/challenge-bonuses-to-tooltip.ts} | 0 .../pipes/score-to-tooltip.pipe.ts | 0 .../src/app/scoreboard/scoreboard.module.ts | 27 +++++++++ .../ticket-list/ticket-list.component.html | 5 +- 22 files changed, 218 insertions(+), 23 deletions(-) create mode 100644 projects/gameboard-ui/src/app/admin/components/game-center/game-center.component.html create mode 100644 projects/gameboard-ui/src/app/admin/components/game-center/game-center.component.scss create mode 100644 projects/gameboard-ui/src/app/admin/components/game-center/game-center.component.ts create mode 100644 projects/gameboard-ui/src/app/admin/pipes/game-classification-to-string.pipe.ts rename projects/gameboard-ui/src/app/{game => scoreboard}/components/scoreboard-team-detail-modal/scoreboard-team-detail-modal.component.html (100%) rename projects/gameboard-ui/src/app/{game => scoreboard}/components/scoreboard-team-detail-modal/scoreboard-team-detail-modal.component.scss (100%) rename projects/gameboard-ui/src/app/{game => scoreboard}/components/scoreboard-team-detail-modal/scoreboard-team-detail-modal.component.ts (100%) rename projects/gameboard-ui/src/app/{game => scoreboard}/components/scoreboard/scoreboard.component.html (100%) rename projects/gameboard-ui/src/app/{game => scoreboard}/components/scoreboard/scoreboard.component.scss (100%) rename projects/gameboard-ui/src/app/{game => scoreboard}/components/scoreboard/scoreboard.component.ts (93%) rename projects/gameboard-ui/src/app/{game/pipes/manual-bonuses-to-tooltip.pipe.ts => scoreboard/pipes/challenge-bonuses-to-tooltip.ts} (100%) rename projects/gameboard-ui/src/app/{game => scoreboard}/pipes/score-to-tooltip.pipe.ts (100%) create mode 100644 projects/gameboard-ui/src/app/scoreboard/scoreboard.module.ts diff --git a/projects/gameboard-ui/src/app/admin/admin.module.ts b/projects/gameboard-ui/src/app/admin/admin.module.ts index 2b2761df..f6f0b8a8 100644 --- a/projects/gameboard-ui/src/app/admin/admin.module.ts +++ b/projects/gameboard-ui/src/app/admin/admin.module.ts @@ -66,6 +66,10 @@ import { SyncStartGameStateDescriptionPipe } from './pipes/sync-start-game-state import { ExternalGameHostPickerComponent } from './components/external-game-host-picker/external-game-host-picker.component'; import { ExternalHostEditorComponent } from './components/external-host-editor/external-host-editor.component'; import { DeleteExternalGameHostModalComponent } from './components/delete-external-game-host-modal/delete-external-game-host-modal.component'; +import { GameCenterComponent } from './components/game-center/game-center.component'; +import { GameClassificationToStringPipe } from './pipes/game-classification-to-string.pipe'; +import { GameModule } from '@/game/game.module'; +import { ScoreboardModule } from '@/scoreboard/scoreboard.module'; @NgModule({ declarations: [ @@ -121,6 +125,8 @@ import { DeleteExternalGameHostModalComponent } from './components/delete-extern ExternalGameHostPickerComponent, ExternalHostEditorComponent, DeleteExternalGameHostModalComponent, + GameCenterComponent, + GameClassificationToStringPipe, ], imports: [ CommonModule, @@ -132,7 +138,16 @@ import { DeleteExternalGameHostModalComponent } from './components/delete-extern { path: '', pathMatch: 'full', redirectTo: 'dashboard' }, { path: 'dashboard', component: DashboardComponent }, { path: 'designer/:id', component: GameEditorComponent }, - { path: "game/:gameId/external", component: ExternalGameAdminComponent }, + { + path: 'game/:gameId', + component: GameCenterComponent, + children: [ + { path: "teams", component: PlayerRegistrarComponent } + ] + }, + { + path: "game/:gameId/external", pathMatch: 'full', component: ExternalGameAdminComponent + }, { path: "practice", component: PracticeComponent, children: [ { path: "", pathMatch: "full", redirectTo: "settings" }, @@ -161,6 +176,7 @@ import { DeleteExternalGameHostModalComponent } from './components/delete-extern CoreModule, ApiModule, UtilityModule, + ScoreboardModule, SponsorsModule, SystemNotificationsModule, ] diff --git a/projects/gameboard-ui/src/app/admin/components/game-center/game-center.component.html b/projects/gameboard-ui/src/app/admin/components/game-center/game-center.component.html new file mode 100644 index 00000000..f36d7987 --- /dev/null +++ b/projects/gameboard-ui/src/app/admin/components/game-center/game-center.component.html @@ -0,0 +1,57 @@ + +
+ +
+

{{ ctx.name }}

+ +

+ {{ ctx.isExternal ? "External" : "Standard" }} + {{ ctx.isPractice ? "Practice" : "Competitive" }} + {{ ctx.isTeamGame ? "Team" : "Individual" }} + Game +

+ +

+ {{ ctx | gameClassificationToString }} +

+ +

+ {{ ctx.executionWindow.start | friendlyDateAndTime }} + — + {{ ctx.executionWindow.end | friendlyDateAndTime }} +

+ +
+
+ Registration Available +
+
Live
+
+
+
+ +
+ + + + + + + + + + + + + + + +
+ +
+
+
+ + + Loading the game... + diff --git a/projects/gameboard-ui/src/app/admin/components/game-center/game-center.component.scss b/projects/gameboard-ui/src/app/admin/components/game-center/game-center.component.scss new file mode 100644 index 00000000..5524c5dd --- /dev/null +++ b/projects/gameboard-ui/src/app/admin/components/game-center/game-center.component.scss @@ -0,0 +1,3 @@ +app-game-card-image { + max-width: 120px; +} diff --git a/projects/gameboard-ui/src/app/admin/components/game-center/game-center.component.ts b/projects/gameboard-ui/src/app/admin/components/game-center/game-center.component.ts new file mode 100644 index 00000000..71f9f7e3 --- /dev/null +++ b/projects/gameboard-ui/src/app/admin/components/game-center/game-center.component.ts @@ -0,0 +1,37 @@ +import { GameCenterContext } from '@/api/admin.models'; +import { AdminService } from '@/api/admin.service'; +import { Game } from '@/api/game-models'; +import { GameService } from '@/api/game.service'; +import { UnsubscriberService } from '@/services/unsubscriber.service'; +import { Component } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { firstValueFrom } from 'rxjs'; + +@Component({ + selector: 'app-game-center', + templateUrl: './game-center.component.html', + styleUrls: ['./game-center.component.scss'], + providers: [UnsubscriberService] +}) +export class GameCenterComponent { + protected game?: Game; + protected gameCenterCtx?: GameCenterContext; + + constructor( + route: ActivatedRoute, + unsub: UnsubscriberService, + private adminService: AdminService, + private gameService: GameService) { + unsub.add( + route.paramMap.subscribe(paramMap => this.load(paramMap.get("gameId"))) + ); + } + + private async load(gameId: string | null) { + if (gameId === null || gameId == this.game?.id) + return; + + this.game = await firstValueFrom(this.gameService.retrieve(gameId)); + this.gameCenterCtx = await this.adminService.getGameCenterContext(gameId); + } +} diff --git a/projects/gameboard-ui/src/app/admin/dashboard/dashboard.component.html b/projects/gameboard-ui/src/app/admin/dashboard/dashboard.component.html index d2a6ef9e..a9783e99 100644 --- a/projects/gameboard-ui/src/app/admin/dashboard/dashboard.component.html +++ b/projects/gameboard-ui/src/app/admin/dashboard/dashboard.component.html @@ -73,6 +73,10 @@
+ + + Game Center + Players diff --git a/projects/gameboard-ui/src/app/admin/game-mapper/game-mapper.component.ts b/projects/gameboard-ui/src/app/admin/game-mapper/game-mapper.component.ts index de6211ff..58e861db 100644 --- a/projects/gameboard-ui/src/app/admin/game-mapper/game-mapper.component.ts +++ b/projects/gameboard-ui/src/app/admin/game-mapper/game-mapper.component.ts @@ -2,7 +2,7 @@ // Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. import { AfterViewInit, Component, ElementRef, EventEmitter, HostListener, Input, OnInit, Output, Renderer2, ViewChild } from '@angular/core'; -import { Observable, Subject } from 'rxjs'; +import { Observable, Subject, firstValueFrom } from 'rxjs'; import { debounceTime, filter, map, switchMap, tap } from 'rxjs/operators'; import { Game } from '../../api/game-models'; import { GameService } from '../../api/game.service'; @@ -20,6 +20,7 @@ import { ToastService } from '@/utility/services/toast.service'; styleUrls: ['./game-mapper.component.scss'] }) export class GameMapperComponent implements OnInit, AfterViewInit { + @Input() gameId?: string; @Input() game!: Game; @Output() specsUpdated = new EventEmitter(); @ViewChild('mapbox') mapboxRef!: ElementRef; @@ -133,11 +134,14 @@ export class GameMapperComponent implements OnInit, AfterViewInit { this.deleted$ = this.deleting$.pipe( switchMap(s => api.delete(s.id)), - tap(() => this.refresh$.next(this.game.id)), + tap(() => this.refresh$.next(this.gameId || this.game.id)), ); } - ngOnInit(): void { + async ngOnInit(): Promise { + if (this.gameId && !this.game) + this.game = await firstValueFrom(this.gameSvc.retrieve(this.gameId)); + this.game.mapUrl = this.game.background ? `${this.config.imagehost}/${this.game.background}` : `${this.config.basehref}assets/map.png` diff --git a/projects/gameboard-ui/src/app/admin/pipes/game-classification-to-string.pipe.ts b/projects/gameboard-ui/src/app/admin/pipes/game-classification-to-string.pipe.ts new file mode 100644 index 00000000..e621de30 --- /dev/null +++ b/projects/gameboard-ui/src/app/admin/pipes/game-classification-to-string.pipe.ts @@ -0,0 +1,25 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +type GameClassification = { + competition: string | null | undefined; + season: string | null | undefined; + track: string | null | undefined; +} + +@Pipe({ name: 'gameClassificationToString' }) +export class GameClassificationToStringPipe implements PipeTransform { + transform(value: GameClassification): string | null { + if (!value) + return null; + + const classificationBits: string[] = [ + value.competition || "", + value.season || "", + value.track || "" + ]; + + return classificationBits + .filter(b => !!b) + .join(" | "); + } +} diff --git a/projects/gameboard-ui/src/app/api/admin.models.ts b/projects/gameboard-ui/src/app/api/admin.models.ts index 94500ef6..07455f1d 100644 --- a/projects/gameboard-ui/src/app/api/admin.models.ts +++ b/projects/gameboard-ui/src/app/api/admin.models.ts @@ -54,6 +54,28 @@ export interface AppActiveTeam { score: number; } +export interface GameCenterContext { + id: string; + name: string; + logo?: string; + competition: string | null; + season: string | null; + track: string | null; + executionWindow: { + start: DateTime, + end: DateTime + }, + isExternal: boolean; + isLive: boolean; + isPractice: boolean; + isRegistrationActive: boolean; + isTeamGame: boolean; + + challengeCount: number; + openTicketCount: number; + pointsAvailable: number; +} + export interface GetSiteOverviewStatsResponse { activeCompetitiveChallenges: number; activePracticeChallenges: number; diff --git a/projects/gameboard-ui/src/app/api/admin.service.ts b/projects/gameboard-ui/src/app/api/admin.service.ts index 0eb83a35..b7489092 100644 --- a/projects/gameboard-ui/src/app/api/admin.service.ts +++ b/projects/gameboard-ui/src/app/api/admin.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; -import { Observable, map, tap } from 'rxjs'; +import { Observable, firstValueFrom, map, tap } from 'rxjs'; import { ApiUrlService } from '@/services/api-url.service'; -import { GetAppActiveChallengesResponse, GetAppActiveTeamsResponse, GetSiteOverviewStatsResponse, SendAnnouncement } from './admin.models'; +import { GameCenterContext, GetAppActiveChallengesResponse, GetAppActiveTeamsResponse, GetSiteOverviewStatsResponse, SendAnnouncement } from './admin.models'; import { PlayerMode } from './player-models'; import { DateTime } from 'luxon'; @@ -39,6 +39,15 @@ export class AdminService { ); } + async getGameCenterContext(gameId: string): Promise { + return firstValueFrom(this.http.get(this.apiUrl.build(`admin/games/${gameId}/game-center`)).pipe( + tap(ctx => { + ctx.executionWindow.start = DateTime.fromJSDate(new Date(ctx.executionWindow.start.toString())); + ctx.executionWindow.end = DateTime.fromJSDate(new Date(ctx.executionWindow.end.toString())); + }) + )); + } + getOverallSiteStats(): Observable { return this.http.get(this.apiUrl.build("admin/stats")); } diff --git a/projects/gameboard-ui/src/app/app-routing.module.ts b/projects/gameboard-ui/src/app/app-routing.module.ts index 405bfee7..bf0233fb 100644 --- a/projects/gameboard-ui/src/app/app-routing.module.ts +++ b/projects/gameboard-ui/src/app/app-routing.module.ts @@ -17,6 +17,7 @@ const routes: Routes = [ }, { path: 'game', + title: "Game", loadChildren: () => import('./game/game.module').then(m => m.GameModule) }, { diff --git a/projects/gameboard-ui/src/app/core/components/game-card-image/game-card-image.component.ts b/projects/gameboard-ui/src/app/core/components/game-card-image/game-card-image.component.ts index 30a4e61e..fb28e3ca 100644 --- a/projects/gameboard-ui/src/app/core/components/game-card-image/game-card-image.component.ts +++ b/projects/gameboard-ui/src/app/core/components/game-card-image/game-card-image.component.ts @@ -5,6 +5,6 @@ import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; templateUrl: './game-card-image.component.html' }) export class GameCardImageComponent { - @Input() game?: { id: string; name: string; logo: string }; + @Input() game?: { id: string; name: string; logo?: string }; @Input() width: string = "100%"; } diff --git a/projects/gameboard-ui/src/app/game/game.module.ts b/projects/gameboard-ui/src/app/game/game.module.ts index a7fe844c..1c9cf9cc 100644 --- a/projects/gameboard-ui/src/app/game/game.module.ts +++ b/projects/gameboard-ui/src/app/game/game.module.ts @@ -25,21 +25,18 @@ import { GamePageComponent } from './pages/game-page/game-page.component'; import { GamespaceQuizComponent } from './gamespace-quiz/gamespace-quiz.component'; import { HubStateToPlayerStatusPipe } from './pipes/hub-state-to-player-status.pipe'; import { IndexToSubmittedAnswersPipe } from './pipes/index-to-submitted-answers.pipe'; -import { ChallengeBonusesToTooltip } from './pipes/manual-bonuses-to-tooltip.pipe'; import { PlayComponent } from './components/play/play.component'; import { PlayerEnrollComponent } from './player-enroll/player-enroll.component'; import { PlayerPresenceComponent } from './player-presence/player-presence.component'; import { PlayerSessionComponent } from './player-session/player-session.component'; -import { ScoreboardComponent } from './components/scoreboard/scoreboard.component'; import { ScoreboardPageComponent } from './pages/scoreboard-page/scoreboard-page.component'; import { ScoreboardTableComponent } from './scoreboard-table/scoreboard-table.component'; -import { ScoreboardTeamDetailModalComponent } from './components/scoreboard-team-detail-modal/scoreboard-team-detail-modal.component'; -import { ScoreToTooltipPipe } from './pipes/score-to-tooltip.pipe'; import { SessionForecastComponent } from './session-forecast/session-forecast.component'; import { SessionStartControlsComponent } from './components/session-start-controls/session-start-controls.component'; import { SessionStartCountdownComponent } from './components/session-start-countdown/session-start-countdown.component'; import { TeamChallengeScoresToChallengeResultTypeCountPipe } from './pipes/team-challenge-scores-to-challenge-result-type-count.pipe'; import { UserIsPlayingGuard } from '@/guards/user-is-playing.guard'; +import { ScoreboardModule } from '@/scoreboard/scoreboard.module'; const MODULE_DECLARATIONS = [ CertificateComponent, @@ -55,19 +52,15 @@ const MODULE_DECLARATIONS = [ HubStateToPlayerStatusPipe, IndexToSubmittedAnswersPipe, LateStartBannerComponent, - ChallengeBonusesToTooltip, PlayComponent, PlayerEnrollComponent, PlayerPresenceComponent, PlayerSessionComponent, - ScoreboardComponent, ScoreboardPageComponent, ScoreboardTableComponent, - ScoreboardTeamDetailModalComponent, SessionForecastComponent, SessionStartControlsComponent, SessionStartCountdownComponent, - ScoreToTooltipPipe, TeamChallengeScoresToChallengeResultTypeCountPipe, ]; @@ -87,7 +80,8 @@ const MODULE_DECLARATIONS = [ { path: ':id', component: GamePageComponent, children: [] } ]), CoreModule, - UtilityModule + UtilityModule, + ScoreboardModule ], exports: [ ChallengeDeployCountdownComponent, diff --git a/projects/gameboard-ui/src/app/game/components/scoreboard-team-detail-modal/scoreboard-team-detail-modal.component.html b/projects/gameboard-ui/src/app/scoreboard/components/scoreboard-team-detail-modal/scoreboard-team-detail-modal.component.html similarity index 100% rename from projects/gameboard-ui/src/app/game/components/scoreboard-team-detail-modal/scoreboard-team-detail-modal.component.html rename to projects/gameboard-ui/src/app/scoreboard/components/scoreboard-team-detail-modal/scoreboard-team-detail-modal.component.html diff --git a/projects/gameboard-ui/src/app/game/components/scoreboard-team-detail-modal/scoreboard-team-detail-modal.component.scss b/projects/gameboard-ui/src/app/scoreboard/components/scoreboard-team-detail-modal/scoreboard-team-detail-modal.component.scss similarity index 100% rename from projects/gameboard-ui/src/app/game/components/scoreboard-team-detail-modal/scoreboard-team-detail-modal.component.scss rename to projects/gameboard-ui/src/app/scoreboard/components/scoreboard-team-detail-modal/scoreboard-team-detail-modal.component.scss diff --git a/projects/gameboard-ui/src/app/game/components/scoreboard-team-detail-modal/scoreboard-team-detail-modal.component.ts b/projects/gameboard-ui/src/app/scoreboard/components/scoreboard-team-detail-modal/scoreboard-team-detail-modal.component.ts similarity index 100% rename from projects/gameboard-ui/src/app/game/components/scoreboard-team-detail-modal/scoreboard-team-detail-modal.component.ts rename to projects/gameboard-ui/src/app/scoreboard/components/scoreboard-team-detail-modal/scoreboard-team-detail-modal.component.ts diff --git a/projects/gameboard-ui/src/app/game/components/scoreboard/scoreboard.component.html b/projects/gameboard-ui/src/app/scoreboard/components/scoreboard/scoreboard.component.html similarity index 100% rename from projects/gameboard-ui/src/app/game/components/scoreboard/scoreboard.component.html rename to projects/gameboard-ui/src/app/scoreboard/components/scoreboard/scoreboard.component.html diff --git a/projects/gameboard-ui/src/app/game/components/scoreboard/scoreboard.component.scss b/projects/gameboard-ui/src/app/scoreboard/components/scoreboard/scoreboard.component.scss similarity index 100% rename from projects/gameboard-ui/src/app/game/components/scoreboard/scoreboard.component.scss rename to projects/gameboard-ui/src/app/scoreboard/components/scoreboard/scoreboard.component.scss diff --git a/projects/gameboard-ui/src/app/game/components/scoreboard/scoreboard.component.ts b/projects/gameboard-ui/src/app/scoreboard/components/scoreboard/scoreboard.component.ts similarity index 93% rename from projects/gameboard-ui/src/app/game/components/scoreboard/scoreboard.component.ts rename to projects/gameboard-ui/src/app/scoreboard/components/scoreboard/scoreboard.component.ts index 9776d5ec..c9010580 100644 --- a/projects/gameboard-ui/src/app/game/components/scoreboard/scoreboard.component.ts +++ b/projects/gameboard-ui/src/app/scoreboard/components/scoreboard/scoreboard.component.ts @@ -4,13 +4,11 @@ import { ScoringService } from '@/services/scoring/scoring.service'; import { ScoreboardData, ScoreboardDataTeam } from '@/services/scoring/scoring.models'; import { ModalConfirmService } from '@/services/modal-confirm.service'; import { ScoreboardTeamDetailModalComponent } from '../scoreboard-team-detail-modal/scoreboard-team-detail-modal.component'; -import { UnsubscriberService } from '@/services/unsubscriber.service'; @Component({ selector: 'app-scoreboard', templateUrl: './scoreboard.component.html', styleUrls: ['./scoreboard.component.scss'], - providers: [UnsubscriberService] }) export class ScoreboardComponent implements OnInit, OnDestroy { @Input() gameId?: string; @@ -29,8 +27,7 @@ export class ScoreboardComponent implements OnInit, OnDestroy { constructor( private modalConfirmService: ModalConfirmService, - private scoreService: ScoringService, - private unsub: UnsubscriberService) { } + private scoreService: ScoringService) { } async ngOnInit() { if (!this.gameId) diff --git a/projects/gameboard-ui/src/app/game/pipes/manual-bonuses-to-tooltip.pipe.ts b/projects/gameboard-ui/src/app/scoreboard/pipes/challenge-bonuses-to-tooltip.ts similarity index 100% rename from projects/gameboard-ui/src/app/game/pipes/manual-bonuses-to-tooltip.pipe.ts rename to projects/gameboard-ui/src/app/scoreboard/pipes/challenge-bonuses-to-tooltip.ts diff --git a/projects/gameboard-ui/src/app/game/pipes/score-to-tooltip.pipe.ts b/projects/gameboard-ui/src/app/scoreboard/pipes/score-to-tooltip.pipe.ts similarity index 100% rename from projects/gameboard-ui/src/app/game/pipes/score-to-tooltip.pipe.ts rename to projects/gameboard-ui/src/app/scoreboard/pipes/score-to-tooltip.pipe.ts diff --git a/projects/gameboard-ui/src/app/scoreboard/scoreboard.module.ts b/projects/gameboard-ui/src/app/scoreboard/scoreboard.module.ts new file mode 100644 index 00000000..4807e92d --- /dev/null +++ b/projects/gameboard-ui/src/app/scoreboard/scoreboard.module.ts @@ -0,0 +1,27 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { CoreModule } from '@/core/core.module'; + +import { ChallengeBonusesToTooltip } from './pipes/challenge-bonuses-to-tooltip'; +import { ScoreboardComponent } from './components/scoreboard/scoreboard.component'; +import { ScoreboardTeamDetailModalComponent } from './components/scoreboard-team-detail-modal/scoreboard-team-detail-modal.component'; +import { ScoreToTooltipPipe } from './pipes/score-to-tooltip.pipe'; + +const PUBLIC_DECLARATIONS = [ + ScoreboardComponent, + ScoreboardTeamDetailModalComponent +]; + +@NgModule({ + declarations: [ + ...PUBLIC_DECLARATIONS, + ChallengeBonusesToTooltip, + ScoreToTooltipPipe + ], + imports: [ + CommonModule, + CoreModule + ], + exports: PUBLIC_DECLARATIONS +}) +export class ScoreboardModule { } diff --git a/projects/gameboard-ui/src/app/support/ticket-list/ticket-list.component.html b/projects/gameboard-ui/src/app/support/ticket-list/ticket-list.component.html index 9f95a441..0a8fb245 100644 --- a/projects/gameboard-ui/src/app/support/ticket-list/ticket-list.component.html +++ b/projects/gameboard-ui/src/app/support/ticket-list/ticket-list.component.html @@ -39,9 +39,8 @@

{{ctx.canManage ? 'Tickets' : 'My Tickets'}}

- +
From e947133274e96e957353eafa84bc87752ba82137 Mon Sep 17 00:00:00 2001 From: Ben Stein Date: Fri, 31 May 2024 15:05:02 -0400 Subject: [PATCH 04/10] Finish batch user create --- .../src/app/admin/admin.module.ts | 2 + .../create-users-modal.component.html | 70 ++++++++++++++++ .../create-users-modal.component.scss | 0 .../create-users-modal.component.ts | 84 +++++++++++++++++++ .../player-registrar.component.html | 4 - .../user-registrar.component.html | 7 ++ .../user-registrar.component.ts | 19 ++++- .../gameboard-ui/src/app/api/user-models.ts | 18 ++++ .../gameboard-ui/src/app/api/user.service.ts | 11 ++- .../modal-content.component.html | 2 +- .../modal-content/modal-content.component.ts | 2 +- projects/gameboard-ui/src/styles.scss | 14 ++++ 12 files changed, 222 insertions(+), 11 deletions(-) create mode 100644 projects/gameboard-ui/src/app/admin/components/create-users-modal/create-users-modal.component.html create mode 100644 projects/gameboard-ui/src/app/admin/components/create-users-modal/create-users-modal.component.scss create mode 100644 projects/gameboard-ui/src/app/admin/components/create-users-modal/create-users-modal.component.ts diff --git a/projects/gameboard-ui/src/app/admin/admin.module.ts b/projects/gameboard-ui/src/app/admin/admin.module.ts index 2b2761df..c8c3fffa 100644 --- a/projects/gameboard-ui/src/app/admin/admin.module.ts +++ b/projects/gameboard-ui/src/app/admin/admin.module.ts @@ -66,6 +66,7 @@ import { SyncStartGameStateDescriptionPipe } from './pipes/sync-start-game-state import { ExternalGameHostPickerComponent } from './components/external-game-host-picker/external-game-host-picker.component'; import { ExternalHostEditorComponent } from './components/external-host-editor/external-host-editor.component'; import { DeleteExternalGameHostModalComponent } from './components/delete-external-game-host-modal/delete-external-game-host-modal.component'; +import { CreateUsersModalComponent } from './components/create-users-modal/create-users-modal.component'; @NgModule({ declarations: [ @@ -121,6 +122,7 @@ import { DeleteExternalGameHostModalComponent } from './components/delete-extern ExternalGameHostPickerComponent, ExternalHostEditorComponent, DeleteExternalGameHostModalComponent, + CreateUsersModalComponent, ], imports: [ CommonModule, diff --git a/projects/gameboard-ui/src/app/admin/components/create-users-modal/create-users-modal.component.html b/projects/gameboard-ui/src/app/admin/components/create-users-modal/create-users-modal.component.html new file mode 100644 index 00000000..3ee4fe9f --- /dev/null +++ b/projects/gameboard-ui/src/app/admin/components/create-users-modal/create-users-modal.component.html @@ -0,0 +1,70 @@ + +
+ Enter space-delimited user GUIDs (globally unique identifiers) in the textbox below to create a new + {{appName}} user account for each. You can use the settings below to control some initial settings for + the created users. +
+ +
+ + + + {{appName}} user IDs may only contain the letters A through F (in upper or lowercase), + hyphens, and digits. The following IDs can't be used: + +
    +
  • {{invalidId}}
  • +
+
+ +
+

Settings

+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ +
+
+ {{userIds.length}} + {{ "user" | pluralizer:userIds.length - invalidIds.length }} will be created. +
+
+
+ + + Loading... + diff --git a/projects/gameboard-ui/src/app/admin/components/create-users-modal/create-users-modal.component.scss b/projects/gameboard-ui/src/app/admin/components/create-users-modal/create-users-modal.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/projects/gameboard-ui/src/app/admin/components/create-users-modal/create-users-modal.component.ts b/projects/gameboard-ui/src/app/admin/components/create-users-modal/create-users-modal.component.ts new file mode 100644 index 00000000..b3de4bd0 --- /dev/null +++ b/projects/gameboard-ui/src/app/admin/components/create-users-modal/create-users-modal.component.ts @@ -0,0 +1,84 @@ +import { Component } from '@angular/core'; +import { UserService } from '@/api/user.service'; +import { ConfigService } from '@/utility/config.service'; +import { TryCreateUsersResponse } from '@/api/user-models'; +import { SponsorService } from '@/api/sponsor.service'; +import { SponsorWithChildSponsors } from '@/api/sponsor-models'; +import { firstValueFrom } from 'rxjs'; +import { GameService } from '@/api/game.service'; +import { SimpleEntity } from '@/api/models'; + +@Component({ + selector: 'app-create-users-modal', + templateUrl: './create-users-modal.component.html', + styleUrls: ['./create-users-modal.component.scss'] +}) +export class CreateUsersModalComponent { + onCreated?: (response: TryCreateUsersResponse) => void | Promise; + + protected allowSubsetCreation = false; + protected createWithSponsorId?: string; + protected unsetDefaultSponsorFlag = false; + + protected appName: string; + protected enrollInGameId?: string; + protected games: SimpleEntity[] = []; + protected hasInvalidIds = false; + protected invalidIds: string[] = []; + protected isWorking = false; + protected placeholder: string; + protected rawText: string = ""; + protected sponsors: SponsorWithChildSponsors[] = []; + protected userIds: string[] = []; + + private invalidIdsRegex = /[a-fA-F0-9-]{2,}/; + private onePerLineRegex = /\s+/gm; + + constructor( + config: ConfigService, + private gameService: GameService, + private sponsorService: SponsorService, + private usersService: UserService) { + this.appName = config.appName; + this.placeholder = "// one ID per line, e.g.:\n\n3496da07-d19e-440d-a246-e35f7b7bfcac\n9a53d8cd-ef88-44c0-96b2-fc8766b518dd\n\n//and so on"; + } + + async ngOnInit() { + this.isWorking = true; + this.games = (await firstValueFrom(this.gameService.list({ "orderBy": "name" }))).map(game => ({ + id: game.id, + name: game.name + })); + this.sponsors = await firstValueFrom(this.sponsorService.listWithChildren()); + this.isWorking = false; + } + + async confirm() { + this.isWorking = true; + const result = await this.usersService.tryCreateMany({ + allowSubsetCreation: this.allowSubsetCreation, + enrollInGameId: this.enrollInGameId, + sponsorId: this.createWithSponsorId, + unsetDefaultSponsorFlag: this.unsetDefaultSponsorFlag, + userIds: this.userIds + }); + this.isWorking = false; + + if (this.onCreated) + this.onCreated(result); + } + + protected handleTextInput() { + this.userIds = this.rawText + .split(this.onePerLineRegex) + .map(entry => entry.trim()) + .filter(entry => entry.length > 2); + + this.invalidIds = []; + for (const id of this.userIds) { + if (!id.match(this.invalidIdsRegex)) { + this.invalidIds.push(id); + } + } + } +} diff --git a/projects/gameboard-ui/src/app/admin/player-registrar/player-registrar.component.html b/projects/gameboard-ui/src/app/admin/player-registrar/player-registrar.component.html index 5cddee8a..5509a711 100644 --- a/projects/gameboard-ui/src/app/admin/player-registrar/player-registrar.component.html +++ b/projects/gameboard-ui/src/app/admin/player-registrar/player-registrar.component.html @@ -47,10 +47,6 @@

Players — {{ctx.game.name}}

prac - | diff --git a/projects/gameboard-ui/src/app/admin/user-registrar/user-registrar.component.html b/projects/gameboard-ui/src/app/admin/user-registrar/user-registrar.component.html index 07b32684..aac21763 100644 --- a/projects/gameboard-ui/src/app/admin/user-registrar/user-registrar.component.html +++ b/projects/gameboard-ui/src/app/admin/user-registrar/user-registrar.component.html @@ -61,6 +61,13 @@

Users

+
+ +
+ Results limited to 200. Refine search term if necessary. diff --git a/projects/gameboard-ui/src/app/admin/user-registrar/user-registrar.component.ts b/projects/gameboard-ui/src/app/admin/user-registrar/user-registrar.component.ts index 419fea73..1b9d8468 100644 --- a/projects/gameboard-ui/src/app/admin/user-registrar/user-registrar.component.ts +++ b/projects/gameboard-ui/src/app/admin/user-registrar/user-registrar.component.ts @@ -5,11 +5,14 @@ import { Component } from '@angular/core'; import { BehaviorSubject, interval, merge, Observable } from 'rxjs'; import { debounceTime, map, switchMap, tap } from 'rxjs/operators'; import { Search } from '../../api/models'; -import { ApiUser, UserRole } from '../../api/user-models'; +import { ApiUser, TryCreateUsersResponse, UserRole } from '../../api/user-models'; import { UserService } from '../../api/user.service'; import { fa } from '@/services/font-awesome.service'; import { SortService } from '@/services/sort.service'; import { SortDirection } from '@/core/models/sort-direction'; +import { ModalConfirmService } from '@/services/modal-confirm.service'; +import { CreateUsersModalComponent } from '../components/create-users-modal/create-users-modal.component'; +import { ToastService } from '@/utility/services/toast.service'; type UserRegistrarSort = "name" | "lastLogin" | "createdOn"; @@ -34,7 +37,9 @@ export class UserRegistrarComponent { constructor( private api: UserService, + private modalService: ModalConfirmService, private sortService: SortService, + private toastService: ToastService ) { this.source$ = merge( this.refresh$, @@ -123,6 +128,18 @@ export class UserRegistrarComponent { this.update(model); } + protected handleAddUsersClick() { + this.modalService.openComponent({ + content: CreateUsersModalComponent, + context: { + onCreated: (response: TryCreateUsersResponse) => { + this.toastService.showMessage(`Created **${response.users.length}** user${response.users.length == 1 ? "s" : ""}.`); + this.refresh$.next(true); + } + } + }); + } + private sortResults(results: ApiUser[], sort: UserRegistrarSort, direction: SortDirection) { switch (sort) { case "lastLogin": diff --git a/projects/gameboard-ui/src/app/api/user-models.ts b/projects/gameboard-ui/src/app/api/user-models.ts index affe4632..a27f7615 100644 --- a/projects/gameboard-ui/src/app/api/user-models.ts +++ b/projects/gameboard-ui/src/app/api/user-models.ts @@ -1,6 +1,7 @@ // 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 { SimpleEntity } from "./models"; import { Sponsor } from "./sponsor-models"; export interface ApiUser { @@ -91,11 +92,28 @@ export interface Announcement { message: string; } +export interface TryCreateUsersRequest { + allowSubsetCreation: boolean; + enrollInGameId?: string; + sponsorId?: string; + unsetDefaultSponsorFlag?: boolean; + userIds: string[]; +} + export interface TryCreateUserResult { isNewUser: boolean; user: ApiUser; } +export interface TryCreateUsersResponse { + users: { + id: string; + name: string; + sponsor: SimpleEntity; + isNewUser: boolean; + }[] +} + // just use this for convenience during the authentication process (see the utiltiy user service). // There are other properties in the profile that may be useful, but just mapping the key ones right now export interface UserOidcProfile { diff --git a/projects/gameboard-ui/src/app/api/user.service.ts b/projects/gameboard-ui/src/app/api/user.service.ts index eda96723..a92045ed 100644 --- a/projects/gameboard-ui/src/app/api/user.service.ts +++ b/projects/gameboard-ui/src/app/api/user.service.ts @@ -3,10 +3,10 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { Observable, Subject } from 'rxjs'; +import { Observable, Subject, firstValueFrom } from 'rxjs'; import { map, tap } from 'rxjs/operators'; import { ConfigService } from '../utility/config.service'; -import { Announcement, ApiUser, ChangedUser, NewUser, TreeNode, TryCreateUserResult, UpdateUserSettingsRequest, UserSettings } from './user-models'; +import { Announcement, ApiUser, ChangedUser, NewUser, TreeNode, TryCreateUserResult, TryCreateUsersRequest, TryCreateUsersResponse, UpdateUserSettingsRequest, UserSettings } from './user-models'; import { LogService } from '@/services/log.service'; import { ApiUrlService } from '@/services/api-url.service'; @@ -46,6 +46,10 @@ export class UserService { ); } + public tryCreateMany(req: TryCreateUsersRequest) { + return firstValueFrom(this.http.post(this.apiUrl.build("users"), req)); + } + public update(model: ChangedUser, disallowedName: string | null = null): Observable { return this.http.put(this.apiUrl.build("user"), model).pipe( map(r => this.transform(r as ApiUser, disallowedName)), @@ -138,8 +142,7 @@ export class UserService { user.roleTag = user.role.split(', ') .map(a => a.substring(0, 1).toUpperCase() + (a.startsWith('d') ? a.substring(1, 2) : '')) - .join('') - ; + .join(''); return user; } } diff --git a/projects/gameboard-ui/src/app/core/components/modal-content/modal-content.component.html b/projects/gameboard-ui/src/app/core/components/modal-content/modal-content.component.html index 9efcf969..189bfdae 100644 --- a/projects/gameboard-ui/src/app/core/components/modal-content/modal-content.component.html +++ b/projects/gameboard-ui/src/app/core/components/modal-content/modal-content.component.html @@ -27,7 +27,7 @@
{{ cancelButtonText || "Cancel" }} diff --git a/projects/gameboard-ui/src/app/core/components/modal-content/modal-content.component.ts b/projects/gameboard-ui/src/app/core/components/modal-content/modal-content.component.ts index 7a276871..906ce076 100644 --- a/projects/gameboard-ui/src/app/core/components/modal-content/modal-content.component.ts +++ b/projects/gameboard-ui/src/app/core/components/modal-content/modal-content.component.ts @@ -1,6 +1,5 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; import { ModalConfirmService } from '@/services/modal-confirm.service'; -import { first, firstValueFrom } from 'rxjs'; @Component({ selector: 'app-modal-content', @@ -14,6 +13,7 @@ export class ModalContentComponent { @Input() subSubtitle?: string; @Input() cancelButtonText?: string; @Input() confirmButtonText?: string; + @Input() confirmDisabled = false; @Input() isDangerConfirm = false; @Output() confirm = new EventEmitter(); diff --git a/projects/gameboard-ui/src/styles.scss b/projects/gameboard-ui/src/styles.scss index c17d1702..59f49eaf 100644 --- a/projects/gameboard-ui/src/styles.scss +++ b/projects/gameboard-ui/src/styles.scss @@ -75,6 +75,16 @@ markdown ol li { margin-left: 1rem; } +select option { + .group-parent { + font-weight: bold; + } + + .group-child { + padding-left: 12px; + } +} + app-root { display: block; min-height: 100vh; @@ -248,6 +258,10 @@ table.gameboard-table { margin: 0; } +.li-style-type-circle { + list-style-type: disc; +} + .form-group { padding: 1.5rem; margin-bottom: 0.5rem; From 8a6b52bc6fa8337a1e4153eb241df2a431565c54 Mon Sep 17 00:00:00 2001 From: Ben Stein Date: Tue, 11 Jun 2024 10:30:31 -0400 Subject: [PATCH 05/10] Add observe links to support tickets. Fix enroll bugs (Admin enroll and loading indicator on error) --- .../player-enroll.component.html | 2 +- .../player-enroll/player-enroll.component.ts | 2 ++ .../src/app/services/router.service.ts | 8 +++++++ .../ticket-support-tools.component.ts | 22 +++++++++++++++++++ 4 files changed, 33 insertions(+), 1 deletion(-) diff --git a/projects/gameboard-ui/src/app/game/player-enroll/player-enroll.component.html b/projects/gameboard-ui/src/app/game/player-enroll/player-enroll.component.html index 2d6db421..7ba7cf2b 100644 --- a/projects/gameboard-ui/src/app/game/player-enroll/player-enroll.component.html +++ b/projects/gameboard-ui/src/app/game/player-enroll/player-enroll.component.html @@ -60,7 +60,7 @@ Enroll - Admin Enroll diff --git a/projects/gameboard-ui/src/app/game/player-enroll/player-enroll.component.ts b/projects/gameboard-ui/src/app/game/player-enroll/player-enroll.component.ts index 897ae3fc..3c50f6c3 100644 --- a/projects/gameboard-ui/src/app/game/player-enroll/player-enroll.component.ts +++ b/projects/gameboard-ui/src/app/game/player-enroll/player-enroll.component.ts @@ -177,6 +177,8 @@ export class PlayerEnrollComponent implements OnInit, OnDestroy { catch (err) { this.errors.push(err); } + + this.isEnrolling = false; } protected async handleUnenroll(p: Player): Promise { diff --git a/projects/gameboard-ui/src/app/services/router.service.ts b/projects/gameboard-ui/src/app/services/router.service.ts index 089549c5..883ed0c9 100644 --- a/projects/gameboard-ui/src/app/services/router.service.ts +++ b/projects/gameboard-ui/src/app/services/router.service.ts @@ -59,6 +59,14 @@ export class RouterService implements OnDestroy { return `/user/${localUserId}/certificates/${mode}/${challengeSpecOrGameId}`; } + public getObserveChallengeUrl(gameId: string, challengeId: string) { + return `/admin/observer/challenges/${gameId}?search=${challengeId}`; + } + + public getObserveTeamsUrl(gameId: string, teamId: string) { + return `admin/observer/teams/${gameId}?search=${teamId}`; + } + public getPracticeAreaWithSearchUrl(searchTerm: string) { return this.router.createUrlTree(["practice"], { queryParams: { term: searchTerm } }); } diff --git a/projects/gameboard-ui/src/app/support/components/ticket-support-tools/ticket-support-tools.component.ts b/projects/gameboard-ui/src/app/support/components/ticket-support-tools/ticket-support-tools.component.ts index 83e865e0..368380e2 100644 --- a/projects/gameboard-ui/src/app/support/components/ticket-support-tools/ticket-support-tools.component.ts +++ b/projects/gameboard-ui/src/app/support/components/ticket-support-tools/ticket-support-tools.component.ts @@ -20,6 +20,21 @@ export interface TicketSupportToolsContext { styleUrls: ['./ticket-support-tools.component.scss'], template: `