Skip to content

Commit

Permalink
v3.19.3 (#186)
Browse files Browse the repository at this point in the history
* Add validation to game editor.

* Don't serve host API key in external host responses.

* Initial work on game center

* Finish batch user create

* Add observe links to support tickets. Fix enroll bugs (Admin enroll and loading indicator on error)

* Allow VM console urls to be built without a name, since they get a default one now.

* Add work for 3.19.3

* Fix decimal precision for questionw eight
  • Loading branch information
sei-bstein authored Jun 13, 2024
1 parent a51a5b0 commit a97b3cf
Show file tree
Hide file tree
Showing 69 changed files with 1,796 additions and 94 deletions.
27 changes: 26 additions & 1 deletion projects/gameboard-ui/src/app/admin/admin.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { RouterModule } from '@angular/router';

import { ApiModule } from '../api/api.module';
import { CoreModule } from '../core/core.module';
import { ScoreboardModule } from '@/scoreboard/scoreboard.module';
import { SponsorsModule } from '@/sponsors/sponsors.module';
import { UtilityModule } from '../utility/utility.module';

Expand All @@ -19,6 +20,7 @@ import { ChallengeBrowserComponent } from './challenge-browser/challenge-browser
import { ChallengeObserverComponent } from './challenge-observer/challenge-observer.component';
import { ChallengeReportComponent } from './challenge-report/challenge-report.component';
import { ChallengeSpecEditorComponent } from './components/challenge-spec-editor/challenge-spec-editor.component';
import { CreateUsersModalComponent } from './components/create-users-modal/create-users-modal.component';
import { DashboardComponent } from './dashboard/dashboard.component';
import { ExternalGameAdminComponent } from './components/external-game-admin/external-game-admin.component';
import { ExternalGameAdminPlayerContextMenuComponent } from './components/external-game-admin-player-context-menu/external-game-admin-player-context-menu.component';
Expand All @@ -30,6 +32,8 @@ import { ExternalSpecIdToChallengePipe } from './pipes/external-specid-to-challe
import { ExternalGamePlayerStatusToFriendlyPipe } from './pipes/external-game-player-status-to-friendly.pipe';
import { FeedbackReportComponent } from './feedback-report/feedback-report.component';
import { GameBonusesConfigComponent } from './components/game-bonuses-config/game-bonuses-config.component';
import { GameCenterComponent } from './components/game-center/game-center.component';
import { GameClassificationToStringPipe } from './pipes/game-classification-to-string.pipe';
import { GameDesignerComponent } from './game-designer/game-designer.component';
import { GameEditorComponent } from './game-editor/game-editor.component';
import { GameMapperComponent } from './game-mapper/game-mapper.component';
Expand Down Expand Up @@ -66,6 +70,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 { GameCenterPlayersComponent } from './components/game-center/game-center-players/game-center-players.component';
import { GameCenterTeamContextMenuComponent } from './components/game-center/game-center-team-context-menu/game-center-team-context-menu.component';
import { GameCenterSettingsComponent } from './components/game-center/game-center-settings/game-center-settings.component';
import { GameCenterTicketsComponent } from './components/game-center/game-center-tickets/game-center-tickets.component';

@NgModule({
declarations: [
Expand All @@ -76,6 +84,7 @@ import { DeleteExternalGameHostModalComponent } from './components/delete-extern
ChallengeObserverComponent,
ChallengeReportComponent,
ChallengeSpecEditorComponent,
CreateUsersModalComponent,
DashboardComponent,
ExternalGameAdminComponent,
ExternalGameAdminPlayerContextMenuComponent,
Expand All @@ -86,6 +95,8 @@ import { DeleteExternalGameHostModalComponent } from './components/delete-extern
ExternalTeamToChallengeCreatedPipe,
ExternalTeamChallengesToIsPredeployablePipe,
FeedbackReportComponent,
GameCenterComponent,
GameClassificationToStringPipe,
GameDesignerComponent,
GameEditorComponent,
GameMapperComponent,
Expand Down Expand Up @@ -121,6 +132,10 @@ import { DeleteExternalGameHostModalComponent } from './components/delete-extern
ExternalGameHostPickerComponent,
ExternalHostEditorComponent,
DeleteExternalGameHostModalComponent,
GameCenterPlayersComponent,
GameCenterTeamContextMenuComponent,
GameCenterSettingsComponent,
GameCenterTicketsComponent,
],
imports: [
CommonModule,
Expand All @@ -132,7 +147,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" },
Expand Down Expand Up @@ -161,6 +185,7 @@ import { DeleteExternalGameHostModalComponent } from './components/delete-extern
CoreModule,
ApiModule,
UtilityModule,
ScoreboardModule,
SponsorsModule,
SystemNotificationsModule,
]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<app-modal-content title="Create Users" (confirm)="confirm()" [confirmDisabled]="!userIds.length"
*ngIf="!isWorking; else loading">
<div class="mb-4">
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.
</div>

<div class="input-controls">
<textarea appAutofocus class="w-100 font-monospace form-control" rows="10" (input)="handleTextInput()"
[placeholder]="placeholder" [(ngModel)]="rawText"></textarea>

<alert *ngIf="invalidIds.length" type="danger" class="mt-3">
{{appName}} user IDs may only contain the letters <strong>A through F</strong> (in upper or lowercase),
<strong>hyphens</strong>, and <strong>digits</strong>. The following IDs can't be used:

<ul class="mt-3">
<li class="li-style-type-circle ml-4" *ngFor="let invalidId of invalidIds">{{invalidId}}</li>
</ul>
</alert>

<div class="settings mt-4">
<h2 class=fs-12>Settings</h2>
<div class="form-check ml-2">
<input type="checkbox" class="form-check-input" name="allow-subset-creation"
[(ngModel)]="allowSubsetCreation">
<label for="allow-subset-creation">Show an error if one of these IDs already exists</label>
</div>

<div class="form-check ml-2">
<input type="checkbox" class="form-check-input" name="unset-default-sponsor-flag"
[(ngModel)]="unsetDefaultSponsorFlag">
<label for="unset-default-sponsor-flag">Don't force users to select their sponsor before playing</label>
</div>

<div class="my-3">
<label for="createWithSponsorId">Create users with sponsor</label>
<select class="form-control" name="createWithSponsorId" [(ngModel)]="createWithSponsorId">
<option [ngValue]="undefined">[the default sponsor]</option>
<ng-container *ngFor="let parent of sponsors">
<option [value]="parent.id" class="group-parent">{{ parent.name }}</option>
<option *ngFor="let child of parent.childSponsors" class="group-child" [value]="child.id">
&nbsp;&nbsp; &nbsp; -
{{ child.name }}
</option>
</ng-container>
</select>
</div>

<div class="my-3">
<label for="enrollInGameId">Enroll users in game</label>
<select class="form-control" name="enrollInGameId" [(ngModel)]="enrollInGameId">
<option [ngValue]="undefined">[don't enroll them]</option>
<option *ngFor="let game of games" [value]="game.id">{{ game.name }}</option>
</select>
</div>
</div>
</div>

<div footer>
<div *ngIf="(userIds.length - invalidIds.length) > 0">
<strong class="text-info">{{userIds.length}}</strong>
{{ "user" | pluralizer:userIds.length - invalidIds.length }} will be created.
</div>
</div>
</app-modal-content>

<ng-template #loading>
<app-spinner>Loading...</app-spinner>
</ng-template>
Original file line number Diff line number Diff line change
@@ -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<void>;

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);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<ng-container *ngIf="!isLoading || !results || !game; else loading">
<ul *ngIf="results?.teams?.items?.length">
<li *ngFor="let team of results?.teams?.items">
<div class="card my-2">
<div class="card-body">
<div class="d-flex">
<div class="d-flex flex-grow-1">
<app-avatar *ngIf="team.players.length == 1" [tooltip]="team.captain.sponsor.name"
[imageUrl]="team.captain.sponsor | sponsorToLogoUri" size="small"
class="mr-2"></app-avatar>
<div>
<h5 class="card-title my-1">{{ team.name }}</h5>
<h6 class="card-subtitle mb-2 text-muted"> {{ team.captain.sponsor.name }}</h6>
</div>
</div>

<app-game-center-team-context-menu *ngIf="game" [team]="team"
[game]="{ id: game.id, name: game.name, isSyncStart: game.requireSynchronizedStart}"></app-game-center-team-context-menu>
</div>

<ul class="d-flex align-items-center" *ngIf="team.players.length > 1">
<li class="mr-3" *ngTemplateOutlet="playerAvatar; context: { $implicit: team.captain}">
</li>
</ul>
</div>
</div>
</li>
</ul>
</ng-container>

<ng-template #playerAvatar let-player>
<app-avatar [imageUrl]="player.sponsor | sponsorToLogoUri" size="tiny" [tooltip]="player.name"></app-avatar>
</ng-template>

<ng-template #noMatches>
<p class="my-2 text-muted text-center">No players match your search.</p>
</ng-template>

<ng-template #loading>
<app-spinner>Finding players...</app-spinner>
</ng-template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Component, Input, OnInit } from '@angular/core';
import { GameCenterTeamsResults } from '@/api/admin.models';
import { AdminService } from '@/api/admin.service';
import { Game } from '@/api/game-models';
import { GameService } from '@/api/game.service';
import { firstValueFrom } from 'rxjs';

@Component({
selector: 'app-game-center-players',
templateUrl: './game-center-players.component.html',
styleUrls: ['./game-center-players.component.scss']
})
export class GameCenterPlayersComponent implements OnInit {
@Input() gameId?: string;

protected game?: Game;
protected isLoading = false;
protected results?: GameCenterTeamsResults;

constructor(
private adminService: AdminService,
private gameService: GameService) { }

async ngOnInit(): Promise<void> {
if (!this.gameId)
throw new Error("Component requires a gameId");

this.game = await firstValueFrom(this.gameService.retrieve(this.gameId));
await this.load();
}

private async load() {
this.isLoading = true;
this.results = await this.adminService.getGameCenterTeams(this.gameId!);
this.isLoading = false;
}
}
Loading

0 comments on commit a97b3cf

Please sign in to comment.