Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v3.29.0 #227

Merged
merged 3 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,23 @@
</button>
</li>
<li class="divider dropdown-divider"></li>
<li role="menuitem">
<a class="dropdown-item btn" *ngIf="observeTeamUrl" [href]="observeTeamUrl">Observe Team</a>
</li>
<li role="menuitem" *ngIf="hasStartedSession && !hasEndedSession">
<button class="dropdown-item btn" (click)="handleExtend(team)">Extend Session</button>
</li>
<li role="menuitem">
<button class="dropdown-item btn" (click)="handleManageBonuses(team)">
Manual Bonuses
</button>
<a class="dropdown-item btn" *ngIf="observeTeamUrl" [href]="observeTeamUrl">Observe Team</a>
</li>

<li role="menuitem" *ngIf="hasEndedSession">
<a class="dropdown-item btn" target="_blank" [href]="certificateUrl">
View Certificate
</a>
</li>
<li role="menuitem">
<button class="dropdown-item btn" (click)="handleManageBonuses(team)">
Manual Bonuses
</button>
</li>

<ng-container *ngIf="!hasStartedSession">
<li class="divider dropdown-divider"></li>
Expand Down Expand Up @@ -72,6 +73,9 @@
</ng-container>

<li class="divider dropdown-divider"></li>
<li role="menuitem">
<button class="dropdown-item btn btn-danger" (click)="handleViewChallenges(team)">Manage Challenges</button>
</li>
<ng-container *ngIf="hasStartedSession; else unenroll">
<li role="menuitem">
<button class="dropdown-item btn btn-danger"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { ExtendTeamsModalComponent } from '../../extend-teams-modal/extend-teams
import { ApiUser } from '@/api/user-models';
import { RouterService } from '@/services/router.service';
import { PlayerMode } from '@/api/player-models';
import { TeamChallengesModalComponent } from '../../team-challenges-modal/team-challenges-modal.component';

export interface TeamSessionResetRequest {
teamId: string;
Expand Down Expand Up @@ -210,4 +211,14 @@ export class GameCenterTeamContextMenuComponent {
modalClasses: ["modal-xl"]
});
}

handleViewChallenges(team: GameCenterTeamsResultsTeam) {
this.modalService.openComponent({
content: TeamChallengesModalComponent,
context: {
teamId: team.id
},
modalClasses: ["modal-xl"]
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,19 +91,9 @@ <h4 class="px-3 mr-1">Timeline</h4>
</ng-container>

<div class="other-tools-container mt-4">
<h4 class="mb-2 px-3">Other tools</h4>
<h4 class="mb-2 px-3">Announce</h4>

<app-announce [teamId]="team.id" placeholderText="Your message (sends only to this player/team)"></app-announce>

<div class="other-tools-buttons px-3">
<button class="btn btn-success btn-sm mr-2" (click)="toggleRawView(!showChallengeYaml)">
<fa-icon [icon]="fa.infoCircle"></fa-icon>
<span>{{ (showChallengeYaml ? "Hide" : "View") }} Data</span>
</button>

<app-yaml-block *ngIf="showChallengeYaml && !isLoadingChallenges"
[source]="teamChallenges"></app-yaml-block>
</div>
</div>
</app-modal-content>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,23 +90,6 @@ export class GameCenterTeamDetailComponent implements OnInit {
modalClasses: ["modal-lg"]
});
}

protected async toggleRawView(isExpanding: boolean) {
if (isExpanding) {
this.isLoadingChallenges = true;

this.teamChallenges = await firstValueFrom(
this
.playerService
.getTeamChallenges(this.team.id)
);

this.isLoadingChallenges = false;
}

this.showChallengeYaml = isExpanding;
}

protected async updateNameChangeRequest(playerId: string, overrideName: string, args: UpdatePlayerNameChangeRequest) {
if (!args.status) {
args.approvedName = overrideName || args.requestedName;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<app-modal-content *ngIf="ctx; else loading" title="Challenges" [subtitle]="ctx.team.name" [subSubtitle]="ctx.game.name"
[hideCancel]=true>
<app-error-div [errors]="errors"></app-error-div>

<ng-container *ngIf="ctx.challengeSpecStatuses.length; else noChallenges">
<alert *ngIf="('Teams_CreateEditDeleteChallenges' | can) || ('Teams_DeployGameResources' | can)" type="danger">
<div class="d-flex align-items-center">
<fa-icon class="d-block mr-2" size="2xl" [icon]="fa.triangleExclamation"></fa-icon>
<div class="flex-grow-1 pl-2">
<p>
<strong>Be <em>very</em> careful with these tools.</strong> Starting a challenge on a player or
team's behalf will affect their cumulative time, and undeploying or purging it will erase all
progress they've made on it. Use with caution!
</p>

<p class="mt-2" *ngIf="!isUnlocked">
Enter the team's admin code <strong>({{ unlockAdminCode }})</strong> below to unlock challenge
management tools.
</p>
</div>
</div>

<div class="form-group p-0 mt-4" *ngIf="!isUnlocked">
<input type="text" class="form-control" [placeholder]="unlockAdminCode"
(input)="handleUnlockAdminCodeInput(unlockAdminCodeInput.value)" #unlockAdminCodeInput>
</div>
</alert>
<table class="table gameboard-table">
<thead>
<tr>
<th>Challenge</th>
<th>Available</th>
<th>Manage</th>
</tr>
</thead>

<tbody>
<tr *ngFor="let challenge of ctx.challengeSpecStatuses">
<td>
{{ challenge.spec.name }}
<div *ngIf="challenge.challengeId" class="d-flex align-items-center text-muted">
<div>{{ challenge.score || 0 }} / {{ challenge.scoreMax }} points &middot;</div>
<button class="btn btn-link p-0" [appCopyOnClick]="challenge.challengeId">{{ {id:
challenge.challengeId} | toSupportCode }}</button>
</div>
</td>
<td>
<div *ngIf="challenge.availabilityRange; else noAvailability"
class="challenge-availability-window">
{{ challenge.availabilityRange.start | friendlyDateAndTime }}
&dash;
{{ challenge.availabilityRange.end | friendlyDateAndTime }}
</div>
</td>
<td>
<div *ngIf="!isWorking; else working">
<div class="gb-button-group btn-group">
<button
*ngIf="challenge.state == 'notStarted' && ('Teams_CreateEditDeleteChallenges' | can)"
class="btn btn-danger" tooltip="Start the challenge"
(click)="handleStartClick(challenge.spec.id)" [disabled]="!isUnlocked">
<fa-icon [icon]="fa.circlePlay"></fa-icon>
</button>
<button *ngIf="challenge.state == 'notDeployed' && ('Teams_DeployGameResources' | can)"
class="btn btn-danger" (click)="handleDeployClick(challenge.challengeId)"
tooltip="Deploy challenge resources" [disabled]="!isUnlocked">
<fa-icon [icon]="fa.play"></fa-icon>
</button>
<button *ngIf="challenge.state == 'deployed' && ('Teams_DeployGameResources' | can)"
class="btn btn-danger" (click)="handleUneployClick(challenge.challengeId)"
tooltip="Undeploy challenge resources" [disabled]="!isUnlocked">
<fa-icon [icon]="fa.stop"></fa-icon>
</button>
<button
*ngIf="challenge.state != 'notStarted' && ('Teams_CreateEditDeleteChallenges' | can)"
class="btn btn-danger" (click)="handlePurgeClick(challenge.challengeId)"
tooltip="Purge this challenge" [disabled]="!isUnlocked">
<fa-icon [icon]="fa.trash"></fa-icon>
</button>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</ng-container>
</app-modal-content>

<ng-template #loading>
<app-spinner>Loading challenges...</app-spinner>
</ng-template>

<ng-template #working>
<div class="d-flex align-items-center justify-content-center">
<app-spinner size="small" color="#fff"></app-spinner>
</div>
</ng-template>

<ng-template #noAvailability>
<div class="text-muted text-center">--</div>
</ng-template>

<ng-template #noChallenges>
<em class="d-block my-2 text-muted text-center">No challenges have been configured for this game.</em>
</ng-template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { Component, inject, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CoreModule } from "@/core/core.module";
import { TeamService } from '@/api/team.service';
import { GetTeamChallengeSpecsStatusesResponse } from '@/api/teams.models';
import { SpinnerComponent } from '@/standalone/core/components/spinner/spinner.component';
import { ErrorDivComponent } from "../../../standalone/core/components/error-div/error-div.component";
import { ToSupportCodePipe } from '@/standalone/core/pipes/to-support-code.pipe';
import { ChallengesService } from '@/api/challenges.service';
import { firstValueFrom, Subject } from 'rxjs';
import { fa } from '@/services/font-awesome.service';

@Component({
selector: 'app-team-challenges-modal',
standalone: true,
imports: [
CommonModule,
CoreModule,
ErrorDivComponent,
SpinnerComponent,
ToSupportCodePipe
],
templateUrl: './team-challenges-modal.component.html',
styleUrls: ['./team-challenges-modal.component.scss']
})
export class TeamChallengesModalComponent implements OnInit {
private challengeService = inject(ChallengesService);
private teamService = inject(TeamService);
teamId!: string;

protected ctx?: GetTeamChallengeSpecsStatusesResponse;
protected errors: any[] = [];
protected fa = fa;
protected isWorking = false;
protected isUnlocked = false;
protected unlockAdminCode = "";
protected unlockAdminCodeInput$ = new Subject<string>();

async ngOnInit(): Promise<void> {
await this.load();
}

async handleDeployClick(challengeId?: string): Promise<void> {
if (!challengeId) {
this.errors.push("No challenge instance.");
}

await this.doChallengeStuff(() => firstValueFrom(this.challengeService.deploy({ id: challengeId! })));
}

async handleUneployClick(challengeId?: string): Promise<void> {
if (!challengeId) {
this.errors.push("No challenge instance.");
}

await this.doChallengeStuff(() => firstValueFrom(this.challengeService.undeploy({ id: challengeId! })));
}

async handlePurgeClick(challengeId?: string) {
if (!challengeId) {
this.errors.push("No challenge instance.");
}

await this.doChallengeStuff(() => this.challengeService.delete(challengeId!));
}

async handleStartClick(challengeSpecId: string): Promise<void> {
await this.doChallengeStuff(() => this.challengeService.start({ teamId: this.teamId, challengeSpecId }));
}

handleUnlockAdminCodeInput(value: string) {
this.isUnlocked = value.toLowerCase() === this.unlockAdminCode.toLowerCase();
}

private async doChallengeStuff<T>(challengeTask: () => Promise<T>) {
try {
this.isWorking = true;
await challengeTask();
}
catch (err) {
this.errors.push(err);
}
finally {
this.isWorking = false;
}

await this.load();
}

private async load(): Promise<void> {
this.errors = [];
if (!this.teamId) {
throw new Error("TeamId is required.");
}

this.ctx = await this.teamService.getChallengeSpecStatuses(this.teamId);
this.unlockAdminCode = this.teamId.substring(0, 6);
}
}
6 changes: 3 additions & 3 deletions projects/gameboard-ui/src/app/api/board.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,14 @@ export class BoardService {
}

public launch(model: NewChallenge): Observable<Challenge> {
return this.http.post<Challenge>(`${this.url}/challenge`, model);
return this.http.post<Challenge>(`${this.url}/challenge/launch`, model);
}

public start(model: ChangedChallenge): Observable<Challenge> {
public deployResources(model: ChangedChallenge): Observable<Challenge> {
return this.http.put<Challenge>(`${this.url}/challenge/start`, model);
}

public stop(model: ChangedChallenge): Observable<Challenge> {
public undeployResources(model: ChangedChallenge): Observable<Challenge> {
return this.http.put<Challenge>(`${this.url}/challenge/stop`, model);
}

Expand Down
14 changes: 13 additions & 1 deletion projects/gameboard-ui/src/app/api/challenges.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import { ChallengeProgressResponse, ChallengeSolutionGuide, GetUserActiveChallen

@Injectable({ providedIn: 'root' })
export class ChallengesService {
private _challengeDeleted$ = new Subject<string>();
public readonly challengeDeleted$ = this._challengeDeleted$.asObservable();

private _challengeDeployStateChanged$ = new Subject<Challenge>();
public readonly challengeDeployStateChanged$ = this._challengeDeployStateChanged$.asObservable();

Expand All @@ -21,6 +24,11 @@ export class ChallengesService {
private apiUrl: ApiUrlService,
private http: HttpClient) { }

public async delete(challengeId: string) {
await firstValueFrom(this.http.delete(this.apiUrl.build(`challenge/${challengeId}`)));
this._challengeDeleted$.next(challengeId);
}

public getActiveChallenges(userId: string): Promise<GetUserActiveChallengesResponse> {
return firstValueFrom(this.http.get<GetUserActiveChallengesResponse>(this.apiUrl.build(`/user/${userId}/challenges/active`)));
}
Expand All @@ -29,8 +37,12 @@ export class ChallengesService {
return this.http.get<Challenge>(this.apiUrl.build(`challenge/${id}`));
}

public start(request: { teamId: string, challengeSpecId: string }): Promise<Challenge> {
return firstValueFrom(this.http.post<Challenge>(this.apiUrl.build(`challenge`), request));
}

public startPlaying(challenge: NewChallenge): Observable<Challenge> {
return this.http.post<Challenge>(this.apiUrl.build("challenge"), challenge).pipe(
return this.http.post<Challenge>(this.apiUrl.build("challenge/launch"), challenge).pipe(
tap(challenge => this._challengeStarted$.next(challenge))
);
}
Expand Down
3 changes: 0 additions & 3 deletions projects/gameboard-ui/src/app/api/player.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,6 @@ export class PlayerService {
return this.http.get<Team>(`${this.url}/team/${id}`);
}

public getTeamChallenges = (id: string): Observable<TeamChallenge[]> =>
this.http.get<TeamChallenge[]>(`${this.url}/team/${id}/challenges`);

public observeTeams(id: string): Observable<any> {
return this.http.get<Team>(`${this.url}/teams/observe/${id}`);
}
Expand Down
6 changes: 5 additions & 1 deletion projects/gameboard-ui/src/app/api/team.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Observable, Subject, firstValueFrom, map, tap } from "rxjs";
import { SessionEndRequest, SessionExtendRequest, Team, TeamSummary } from "./player-models";
import { AddToTeamResponse, AdminEnrollTeamRequest, AdminEnrollTeamResponse, AdminExtendTeamSessionResponse, AdvanceTeamsRequest, RemoveFromTeamResponse, TeamSessionResetType, TeamSessionUpdate } from "./teams.models";
import { AddToTeamResponse, AdminEnrollTeamRequest, AdminEnrollTeamResponse, AdminExtendTeamSessionResponse, AdvanceTeamsRequest, GetTeamChallengeSpecsStatusesResponse, RemoveFromTeamResponse, TeamChallengeSpecStatus, TeamSessionResetType, TeamSessionUpdate } from "./teams.models";
import { ApiUrlService } from "@/services/api-url.service";
import { unique } from "../../tools/tools";
import { GamePlayState } from "./game-models";
Expand Down Expand Up @@ -75,6 +75,10 @@ export class TeamService {
return this.http.get<Team>(this.apiUrl.build(`/team/${teamId}`));
}

public getChallengeSpecStatuses(teamId: string) {
return firstValueFrom(this.http.get<GetTeamChallengeSpecsStatusesResponse>(this.apiUrl.build(`/team/${teamId}/challenges`)));
}

public getMailMetadata(gameId: string) {
return this.http.get<TeamSummary[]>(this.apiUrl.build(`/teams/${gameId}`));
}
Expand Down
Loading