From e700334c44e05a3d3b29a2ba5631f80cd3a72ff2 Mon Sep 17 00:00:00 2001 From: Ben Stein <115497763+sei-bstein@users.noreply.github.com> Date: Mon, 2 Dec 2024 14:16:18 -0500 Subject: [PATCH] v3.25.0 (#208) * Make auto logout the default if not overridden by helm. * Hide rerank if user doesn't have permission. * Various bug fixes for competitive * Make game center -> tickets show total ticket count * Roll back default favicon changes. Refine info bubble styling. * Bug fixes to challenge deploy status in practice * Support ticket toasts now link to the ticket. * Ticket activity sortable * Resolves #540 and some other cleanup * Fix duplicate login bugs, various testing bugs * Misc formatting and cleanup * Misc formatting and hide new feedback template picker * Select first unfinished challenge section in practice mode --- .../admin-player-session.component.html | 97 ------------ .../admin-player-session.component.scss | 12 -- .../admin-player-session.component.ts | 104 ------------- .../src/app/admin/admin.module.ts | 8 +- .../admin-enroll-team-modal.component.ts | 4 +- .../extend-teams-modal.component.html | 6 +- .../extend-teams-modal.component.ts | 2 +- .../feedback-editor.component.html | 4 +- .../game-center-settings.component.html | 9 +- .../game-center-settings.component.scss | 4 + ...me-center-team-context-menu.component.html | 31 +++- ...game-center-team-context-menu.component.ts | 73 +++++++-- .../game-center-team-detail.component.html | 11 +- .../game-center-team-detail.component.ts | 21 ++- .../game-center-teams.component.html | 27 ++-- .../game-center-teams.component.ts | 41 ++--- .../game-center/game-center.component.html | 2 +- .../game-center/game-center.models.ts | 1 + .../team-admin-context-menu.component.html | 77 ---------- .../team-admin-context-menu.component.scss | 14 -- .../team-admin-context-menu.component.ts | 142 ------------------ .../admin/dashboard/dashboard.component.html | 4 +- .../user-api-keys.component.html | 4 +- .../user-registrar.component.html | 8 +- .../user-registrar.component.ts | 10 +- .../src/app/api/player.service.ts | 8 - .../src/app/api/support.service.ts | 6 +- .../gameboard-ui/src/app/api/team.service.ts | 33 +++- .../gameboard-ui/src/app/api/teams.models.ts | 17 ++- .../app/api/user-role-permissions.service.ts | 26 +++- .../gameboard-ui/src/app/api/user.service.ts | 31 +--- .../gameboard-ui/src/app/app.component.ts | 2 +- .../src/app/components/nav/nav.component.html | 4 +- .../cumulative-time-clock.component.html | 5 +- ...meboard-performance-summary.component.html | 10 +- .../ticket-list/ticket-list.component.html | 4 +- .../ticket-list/ticket-list.component.ts | 8 +- .../src/app/core/pipes/can.pipe.ts | 2 +- ...meboard-performance-summary.component.html | 10 +- .../session-start-controls.component.ts | 9 +- .../gamespace-quiz.component.html | 40 +++-- .../gamespace-quiz.component.scss | 8 + .../gamespace-quiz.component.ts | 38 ----- .../pages/game-page/game-page.component.ts | 7 +- .../gameboard-page.component.html | 9 +- .../gameboard-page.component.scss | 8 +- .../app/game/pipes/has-pending-name.pipe.ts | 1 - .../player-enroll.component.html | 14 +- .../player-enroll/player-enroll.component.ts | 5 +- .../player-presence.component.ts | 22 +-- .../player-session.component.html | 8 +- .../player-session.component.ts | 10 +- .../src/app/guards/game-is-started.guard.ts | 4 +- .../src/app/guards/user-is-playing.guard.ts | 2 +- .../src/app/home/oidc/oidc.component.html | 2 +- .../practice-challenge-list.component.html | 15 +- .../practice-challenge-list.component.ts | 8 +- .../practice-session.component.html | 2 +- .../practice-session.component.ts | 10 +- .../gameboard-ui/src/app/prac/prac.module.ts | 2 + .../src/app/prac/practice.models.ts | 22 ++- .../src/app/services/api-date-time.service.ts | 4 +- .../app/services/app-notifications.service.ts | 13 +- .../src/app/services/font-awesome.service.ts | 2 +- .../src/app/services/local-storage.service.ts | 2 +- .../services/signalR/support-hub.service.ts | 10 +- ...ponsor-with-children-picker.component.html | 18 ++- .../info-bubble/info-bubble.component.scss | 16 ++ .../info-bubble/info-bubble.component.ts | 22 +++ .../directives/if-has-permission.directive.ts | 4 +- .../challenge-questions.component.ts | 7 +- .../games/components/play/play.component.html | 141 +++++++++-------- .../games/components/play/play.component.ts | 80 +++++----- .../user-picker/user-picker.component.html | 19 +++ .../user-picker/user-picker.component.scss | 0 .../user-picker/user-picker.component.ts | 45 ++++++ .../ticket-details.component.html | 26 ++-- .../ticket-details.component.ts | 13 +- .../ticket-form/ticket-form.component.ts | 2 +- .../components/settings/settings.component.ts | 1 - .../src/app/users/users.module.ts | 2 +- .../src/app/utility/admin.guard.ts | 2 +- .../src/app/utility/auth.service.ts | 4 +- .../src/app/utility/config.service.ts | 4 +- .../src/app/utility/services/toast.service.ts | 7 +- .../src/app/utility/user.service.ts | 22 +-- .../src/environments/environment.ts | 2 - projects/gameboard-ui/src/index.html | 5 +- projects/gameboard-ui/src/scss/_toastify.scss | 4 + .../gameboard-ui/src/scss/_variables.scss | 1 + projects/gameboard-ui/src/styles.scss | 4 +- 91 files changed, 726 insertions(+), 884 deletions(-) delete mode 100644 projects/gameboard-ui/src/app/admin/admin-player-session/admin-player-session.component.html delete mode 100644 projects/gameboard-ui/src/app/admin/admin-player-session/admin-player-session.component.scss delete mode 100644 projects/gameboard-ui/src/app/admin/admin-player-session/admin-player-session.component.ts delete mode 100644 projects/gameboard-ui/src/app/admin/components/team-admin-context-menu/team-admin-context-menu.component.html delete mode 100644 projects/gameboard-ui/src/app/admin/components/team-admin-context-menu/team-admin-context-menu.component.scss delete mode 100644 projects/gameboard-ui/src/app/admin/components/team-admin-context-menu/team-admin-context-menu.component.ts create mode 100644 projects/gameboard-ui/src/app/standalone/core/components/info-bubble/info-bubble.component.scss create mode 100644 projects/gameboard-ui/src/app/standalone/core/components/info-bubble/info-bubble.component.ts create mode 100644 projects/gameboard-ui/src/app/standalone/users/user-picker/user-picker.component.html create mode 100644 projects/gameboard-ui/src/app/standalone/users/user-picker/user-picker.component.scss create mode 100644 projects/gameboard-ui/src/app/standalone/users/user-picker/user-picker.component.ts diff --git a/projects/gameboard-ui/src/app/admin/admin-player-session/admin-player-session.component.html b/projects/gameboard-ui/src/app/admin/admin-player-session/admin-player-session.component.html deleted file mode 100644 index 7a6de1d64..000000000 --- a/projects/gameboard-ui/src/app/admin/admin-player-session/admin-player-session.component.html +++ /dev/null @@ -1,97 +0,0 @@ - - - - -

Timeline

-
- -
- - - -
-

Advancement

-
-
-
Advanced from
-
{{team.advancedFromGame.name}}
-
- -
-
Played as
-
{{team.advancedFromPlayer?.name}}
-
- -
-
Team
-
{{team.advancedFromPlayer?.name}}
-
- -
-
Score
-
{{team.advancedWithScore || 0}}
-
-
-
- -

Session Extension

-
-
- -
- -
- -
-
-
-
- -
- -
- -
-
-
-
- -
-

Other tools

- - - -
- - - Unenroll -
-
- - -
-
- -
-
-
- -
- - -
- -
-
diff --git a/projects/gameboard-ui/src/app/admin/admin-player-session/admin-player-session.component.scss b/projects/gameboard-ui/src/app/admin/admin-player-session/admin-player-session.component.scss deleted file mode 100644 index a55726d73..000000000 --- a/projects/gameboard-ui/src/app/admin/admin-player-session/admin-player-session.component.scss +++ /dev/null @@ -1,12 +0,0 @@ -.yaml-container { - margin: 1.5rem 0 0 1.5rem; - padding-bottom: 1.5rem; -} - -.advancement-container { - h6 { - font-size: 0.9rem; - font-weight: bold; - text-transform: uppercase; - } -} diff --git a/projects/gameboard-ui/src/app/admin/admin-player-session/admin-player-session.component.ts b/projects/gameboard-ui/src/app/admin/admin-player-session/admin-player-session.component.ts deleted file mode 100644 index 56e18a306..000000000 --- a/projects/gameboard-ui/src/app/admin/admin-player-session/admin-player-session.component.ts +++ /dev/null @@ -1,104 +0,0 @@ -// 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, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { faInfoCircle } from '@fortawesome/free-solid-svg-icons'; -import { Observable, firstValueFrom } from 'rxjs'; -import { first, tap } from 'rxjs/operators'; -import { Player, Team, TimeWindow } from '@/api/player-models'; -import { PlayerService } from '@/api/player.service'; -import { GameSessionService } from '@/services/game-session.service'; -import { TeamAdminContextMenuSessionResetRequest } from '../components/team-admin-context-menu/team-admin-context-menu.component'; -import { DateTime } from 'luxon'; -import { ModalConfirmService } from '@/services/modal-confirm.service'; -import { ExtendTeamsModalComponent } from '../components/extend-teams-modal/extend-teams-modal.component'; -import { GameService } from '@/api/game.service'; - -@Component({ - selector: 'app-admin-player-session', - templateUrl: './admin-player-session.component.html', - styleUrls: ['./admin-player-session.component.scss'] -}) -export class PlayerSessionComponent implements OnInit { - @Input() player!: Player; - @Output() onManageManualBonusesRequest = new EventEmitter(); - @Output() onUnenrollRequest = new EventEmitter(); - @Output() onResetSessionRequest = new EventEmitter(); - - team$!: Observable; - team!: Team; - canUnenroll = false; - showRaw = false; - statusText = "Loading your session..."; - faInfo = faInfoCircle; - protected errors: any[] = []; - - protected isoDateExtension = ""; - protected durationExtensionInMinutes?: number; - protected isExtending = false; - protected isLoadingChallenges = false; - - constructor( - private api: PlayerService, - private gameService: GameService, - private sessionService: GameSessionService, - private modalService: ModalConfirmService, - ) { } - - ngOnInit(): void { - this.team$ = this.api.getTeam(this.player.teamId).pipe( - tap(t => this.team = t), - tap(t => this.canUnenroll = this.sessionService.canUnenrollSession(new TimeWindow(t.sessionBegin, t.sessionEnd))), - tap(t => { - t.sessionBegin = new Date(t.sessionBegin); - t.sessionEnd = new Date(t.sessionEnd); - this.isoDateExtension = t.sessionEnd.toISOString(); - }) - ); - } - - // extend by ISO timestamp - async extend(team: Team): Promise { - const friendlySessionEnd = DateTime.fromISO(this.isoDateExtension); - const extensionDuration = friendlySessionEnd.diffNow("minutes"); - await this.extendByDuration(team, Math.floor(extensionDuration.minutes)); - } - - async extendByDuration(team: Team, extensionInMinutes?: number) { - if (!extensionInMinutes) { - return; - } - - const game = await firstValueFrom(this.gameService.retrieve(team.gameId)); - - this.modalService.openComponent({ - content: ExtendTeamsModalComponent, - context: { - extensionInMinutes: extensionInMinutes, - game: { - id: team.gameId, - name: game.name, - isTeamGame: game.isTeamGame - }, - teamIds: [team.teamId] - }, - modalClasses: ["modal-lg"] - }); - } - - toggleRawView(isExpanding: boolean): void { - if (isExpanding) { - this.team.challenges = []; - this.isLoadingChallenges = true; - - this.api.getTeamChallenges(this.team.teamId) - .pipe(first()) - .subscribe(c => { - this.team.challenges = c; - this.isLoadingChallenges = false; - }); - } - - this.showRaw = isExpanding; - } -} diff --git a/projects/gameboard-ui/src/app/admin/admin.module.ts b/projects/gameboard-ui/src/app/admin/admin.module.ts index 83b83f3d2..635365db0 100644 --- a/projects/gameboard-ui/src/app/admin/admin.module.ts +++ b/projects/gameboard-ui/src/app/admin/admin.module.ts @@ -49,10 +49,8 @@ import { GameMapEditorComponent } from './components/game-map-editor/game-map-ed import { GameYamlImportModalComponent } from './components/game-yaml-import-modal/game-yaml-import-modal.component'; import { ManageManualChallengeBonusesModalComponent } from './components/manage-manual-challenge-bonuses-modal/manage-manual-challenge-bonuses-modal.component'; import { ManageManualChallengeBonusesComponent } from './components/manage-manual-challenge-bonuses/manage-manual-challenge-bonuses.component'; -import { PlayerSessionComponent } from './admin-player-session/admin-player-session.component'; import { SiteOverviewStatsComponent } from './components/site-overview-stats/site-overview-stats.component'; import { SupportSettingsComponent } from './components/support-settings/support-settings.component'; -import { TeamAdminContextMenuComponent } from './components/team-admin-context-menu/team-admin-context-menu.component'; import { TeamCenterComponent } from './components/team-center/team-center.component'; import { TeamListCardComponent } from './components/team-list-card/team-list-card.component'; import { DashboardComponent } from './dashboard/dashboard.component'; @@ -90,6 +88,7 @@ import { SpinnerComponent } from '@/standalone/core/components/spinner/spinner.c import { ToSupportCodePipe } from '@/standalone/core/pipes/to-support-code.pipe'; import { IfHasPermissionDirective } from '@/standalone/directives/if-has-permission.directive'; import { FeedbackTemplatePickerComponent } from "../feedback/components/feedback-template-picker/feedback-template-picker.component"; +import { UserPickerComponent } from '@/standalone/users/user-picker/user-picker.component'; @NgModule({ declarations: [ @@ -127,7 +126,6 @@ import { FeedbackTemplatePickerComponent } from "../feedback/components/feedback GameBonusesConfigComponent, ParticipationReportComponent, PlayerNamesComponent, - PlayerSessionComponent, PlayerSponsorReportComponent, PracticeComponent, PracticeSettingsComponent, @@ -136,7 +134,6 @@ import { FeedbackTemplatePickerComponent } from "../feedback/components/feedback SponsorBrowserComponent, SupportAutoTagAdminComponent, SupportReportLegacyComponent, - TeamAdminContextMenuComponent, TeamObserverComponent, UserApiKeysComponent, UserRegistrarComponent, @@ -230,7 +227,8 @@ import { FeedbackTemplatePickerComponent } from "../feedback/components/feedback SafeUrlPipe, SpinnerComponent, ToSupportCodePipe, - FeedbackTemplatePickerComponent + FeedbackTemplatePickerComponent, + UserPickerComponent, ] }) diff --git a/projects/gameboard-ui/src/app/admin/components/admin-enroll-team-modal/admin-enroll-team-modal.component.ts b/projects/gameboard-ui/src/app/admin/components/admin-enroll-team-modal/admin-enroll-team-modal.component.ts index 77819b6c3..27abc344a 100644 --- a/projects/gameboard-ui/src/app/admin/components/admin-enroll-team-modal/admin-enroll-team-modal.component.ts +++ b/projects/gameboard-ui/src/app/admin/components/admin-enroll-team-modal/admin-enroll-team-modal.component.ts @@ -34,7 +34,7 @@ export class AdminEnrollTeamModalComponent implements OnInit { protected searchTerm = ""; protected selectedUsers: ApiUser[] = []; protected typeaheadSearch$ = new Observable((observer: Observer) => observer.next(this.searchTerm)).pipe( - filter(s => s?.length >= 3), + filter(s => s?.length >= 2), switchMap(search => this.userService.list({ eligibleForGameId: this.game!.id, excludeIds: [...this.selectedUsers.map(u => u.id)], @@ -68,7 +68,7 @@ export class AdminEnrollTeamModalComponent implements OnInit { try { const userIds = this.selectedUsers.map(u => u.id); - const result = await firstValueFrom(this.teamService.adminEnroll({ userIds, gameId: this.game.id })); + const result = await this.teamService.adminEnroll({ userIds, gameId: this.game.id }); if (this.onConfirm) await this.onConfirm(result); diff --git a/projects/gameboard-ui/src/app/admin/components/extend-teams-modal/extend-teams-modal.component.html b/projects/gameboard-ui/src/app/admin/components/extend-teams-modal/extend-teams-modal.component.html index ef6962479..a98fefa02 100644 --- a/projects/gameboard-ui/src/app/admin/components/extend-teams-modal/extend-teams-modal.component.html +++ b/projects/gameboard-ui/src/app/admin/components/extend-teams-modal/extend-teams-modal.component.html @@ -25,8 +25,8 @@ If you use a negative value for the extension length, you'll decrease the amount of time - available to the {{ game?.isTeamGame ? "teams" : "players" }} shown here. Ensure the session times below - are what you expect. + available to the {{ (game?.maxTeamSize || 1) > 1 ? "teams" : "players" }} shown here. Ensure the session + times below are what you expect. @@ -54,7 +54,7 @@ + [class.text-success]="!(newSessionEnd | dateTimeIsPast)"> {{ newSessionEnd | datetimeToDate | friendlyDateAndTime }} ) diff --git a/projects/gameboard-ui/src/app/admin/components/extend-teams-modal/extend-teams-modal.component.ts b/projects/gameboard-ui/src/app/admin/components/extend-teams-modal/extend-teams-modal.component.ts index 71eae9ad2..02c1e2f03 100644 --- a/projects/gameboard-ui/src/app/admin/components/extend-teams-modal/extend-teams-modal.component.ts +++ b/projects/gameboard-ui/src/app/admin/components/extend-teams-modal/extend-teams-modal.component.ts @@ -13,8 +13,8 @@ import { Team } from '@/api/player-models'; export class ExtendTeamsModalComponent implements OnInit { game?: { id: string; + maxTeamSize: number; name: string; - isTeamGame: boolean; }; extensionInMinutes = 30; onExtend?: () => Promise | Observable; diff --git a/projects/gameboard-ui/src/app/admin/components/feedback-editor/feedback-editor.component.html b/projects/gameboard-ui/src/app/admin/components/feedback-editor/feedback-editor.component.html index 72a08328a..34490e6e5 100644 --- a/projects/gameboard-ui/src/app/admin/components/feedback-editor/feedback-editor.component.html +++ b/projects/gameboard-ui/src/app/admin/components/feedback-editor/feedback-editor.component.html @@ -26,10 +26,10 @@ -
+ - diff --git a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-settings/game-center-settings.component.html b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-settings/game-center-settings.component.html index 1e4a9265c..504d4e0a6 100644 --- a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-settings/game-center-settings.component.html +++ b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-settings/game-center-settings.component.html @@ -43,7 +43,7 @@
- Featured games always show at the top of the homepage + Featured games always show at the top of the homepage
@@ -148,6 +148,7 @@
+
@@ -395,9 +396,9 @@

Registration


- - - + + +
diff --git a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-settings/game-center-settings.component.scss b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-settings/game-center-settings.component.scss index fd4bc9ee3..9c3840dc4 100644 --- a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-settings/game-center-settings.component.scss +++ b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-settings/game-center-settings.component.scss @@ -30,6 +30,10 @@ label { font-size: 87.5%; } +.form-group small { + color: $muted; +} + .help-text { font-style: italic; font-size: 0.8em; diff --git a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-team-context-menu/game-center-team-context-menu.component.html b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-team-context-menu/game-center-team-context-menu.component.html index 5acdabe8f..a255889ef 100644 --- a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-team-context-menu/game-center-team-context-menu.component.html +++ b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-team-context-menu/game-center-team-context-menu.component.html @@ -14,12 +14,15 @@
  • -
  • +
  • +
  • + +
  • +
  • +
  • @@ -92,8 +101,20 @@ -
  • - +
  • +
  • + + + + + NOTE: This feature is currently only available for users who haven't yet registered for + the game. If you want to add a player to this team and can't find them here, ensure they're not registered + (or unenroll them if they are) and then come back to try again. + + + + diff --git a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-team-context-menu/game-center-team-context-menu.component.ts b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-team-context-menu/game-center-team-context-menu.component.ts index 5a6289df8..b74fb3468 100644 --- a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-team-context-menu/game-center-team-context-menu.component.ts +++ b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-team-context-menu/game-center-team-context-menu.component.ts @@ -1,7 +1,7 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { Component, EventEmitter, Input, Output, TemplateRef, ViewChild } from '@angular/core'; import { firstValueFrom } from 'rxjs'; import { TeamSessionResetType } from '@/api/teams.models'; -import { SimpleEntity } from '@/api/models'; +import { ApiError, SimpleEntity } from '@/api/models'; import { fa } from '@/services/font-awesome.service'; import { ClipboardService } from '@/utility/services/clipboard.service'; import { ToastService } from '@/utility/services/toast.service'; @@ -14,6 +14,8 @@ import { TeamService } from '@/api/team.service'; import { GameCenterTeamsResultsTeam } from '../game-center.models'; import { GameCenterTeamDetailComponent } from '../game-center-team-detail/game-center-team-detail.component'; import { GameSessionService } from '@/services/game-session.service'; +import { ExtendTeamsModalComponent } from '../../extend-teams-modal/extend-teams-modal.component'; +import { ApiUser } from '@/api/user-models'; export interface TeamSessionResetRequest { teamId: string; @@ -26,12 +28,14 @@ export interface TeamSessionResetRequest { styleUrls: ['./game-center-team-context-menu.component.scss'] }) export class GameCenterTeamContextMenuComponent { - @Input() game?: { id: string; name: string; isSyncStart: boolean; isTeamGame: boolean }; + @Input() game?: { id: string; name: string; isSyncStart: boolean; maxTeamSize: number }; @Input() team?: GameCenterTeamsResultsTeam; + @Output() error = new EventEmitter(); @Output() teamUpdated = new EventEmitter(); protected fa = fa; protected hasStartedSession = false; + @ViewChild("addPlayerModal") addPlayerModalTemplate?: TemplateRef; constructor( private clipboard: ClipboardService, @@ -58,14 +62,14 @@ export class GameCenterTeamContextMenuComponent { // this is all really window-dressing around the "reset" operation, which we describe as an unenroll if the team's session hasn't started const isUnenroll = request.resetType === "unenrollAndArchiveChallenges" && this.gameSessionService.canUnerollSessionWithEpochMs(this.team.session); - let confirmMessage = `Are you sure you want to unenroll ${this.game.isTeamGame ? " team" : ""} **${this.team.name}** from the game?`; + let confirmMessage = `Are you sure you want to unenroll ${this.game.maxTeamSize > 1 ? " team" : ""} **${this.team.name}** from the game?`; let confirmTitle = `Unenroll ${this.team.name}?`; let confirmToast = `**${this.team.name}** has been unenrolled.`; if (!isUnenroll) { - confirmMessage = `Are you sure you want to reset the session for ${this.game.isTeamGame ? " team" : ""} **${this.team.name}**?`; + confirmMessage = `Are you sure you want to reset the session for ${this.game.maxTeamSize > 1 ? " team" : ""} **${this.team.name}**?`; confirmTitle = `Reset ${this.team.name}'s session?`; - confirmToast = `${this.game.isTeamGame ? "Team " : ""}**${this.team.name}**'s session has been reset.`; + confirmToast = `${this.game.maxTeamSize > 1 ? "Team " : ""}**${this.team.name}**'s session has been reset.`; // accommodate various "types" of reset that can happen (e.g. keep challenges, don't keep challenges, destroy the universe and the unenroll) if (request.resetType === "preserveChallenges") @@ -93,11 +97,36 @@ export class GameCenterTeamContextMenuComponent { this.toastService.showMessage(`Copied ${description} **${text}** to your clipboard.`); } + protected async handleAddPlayerClick(team: GameCenterTeamsResultsTeam) { + if (!this.addPlayerModalTemplate) { + throw new Error("Couldn't load the template."); + } + + this.modalService.openTemplate(this.addPlayerModalTemplate); + } + + protected async handleAddUserConfirm(team: GameCenterTeamsResultsTeam, user: ApiUser) { + this.modalService.hide(); + const addedPlayer = await this.teamService.addToTeam({ teamId: team.id, userId: user.id }); + this.teamUpdated.emit(this.team); + this.toastService.showMessage(`User **${addedPlayer.user.name}** has joined team **${team.name}**!`); + } + protected async handleDeployResources(team: SimpleEntity) { await this.gameService.deployResources(this.game!.id, [team.id]); this.toastService.showMessage(`Resources are being deployed for **${team.name}**.`); } + protected async handleExtend(team: SimpleEntity) { + this.modalService.openComponent({ + content: ExtendTeamsModalComponent, + context: { + game: this.game, + teamIds: [team.id] + } + }); + } + async handleManageBonuses(team: SimpleEntity) { this.modalService.openComponent({ content: ManageManualChallengeBonusesModalComponent, @@ -113,6 +142,32 @@ export class GameCenterTeamContextMenuComponent { this.teamUpdated.emit(team); } + async handleStartSession(team: SimpleEntity) { + try { + await firstValueFrom(this.playerService.startPlayerId(this.team!.captain.id)); + this.teamUpdated.emit(team); + this.toastService.showMessage(`Session started for **${team.name}**`); + } + catch (err: any) { + if ("message" in err) { + this.error.emit([(err as ApiError).message]); + } + else { + this.error.emit([JSON.stringify(err)]); + } + } + } + + protected handleUnenrollClick(team: SimpleEntity) { + this.modalService.openConfirm({ + bodyContent: `Are you sure you want to unenroll **${team.name}**?`, + renderBodyAsMarkdown: true, + subtitle: team.name, + title: "Unenroll " + ((this.game?.maxTeamSize || 0) > 1 ? "Team" : "Player"), + onConfirm: async () => this.handleResetRequest(team, 'unenrollAndArchiveChallenges') + }); + } + async handleUpdateReady(team: SimpleEntity, isReady: boolean) { await firstValueFrom(this.syncStartService.updateTeamReadyState(team.id, { isReady })); this.teamUpdated.emit(team); @@ -124,12 +179,6 @@ export class GameCenterTeamContextMenuComponent { ); } - async handleStartSession(team: SimpleEntity) { - await firstValueFrom(this.playerService.startPlayerId(this.team!.captain.id)); - this.teamUpdated.emit(team); - this.toastService.showMessage(`Session started for **${team.name}**`); - } - async handleView(team: GameCenterTeamsResultsTeam) { this.modalService.openComponent({ content: GameCenterTeamDetailComponent, diff --git a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-team-detail/game-center-team-detail.component.html b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-team-detail/game-center-team-detail.component.html index 17586eeaa..54dad34b0 100644 --- a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-team-detail/game-center-team-detail.component.html +++ b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-team-detail/game-center-team-detail.component.html @@ -15,7 +15,7 @@
    Score
    -

    Name Management

    +

    Team Management

    • @@ -47,8 +47,13 @@

      Name Management

    - + +
    @@ -84,7 +89,7 @@

    Timeline

    -
    +

    Other tools

    diff --git a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-team-detail/game-center-team-detail.component.ts b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-team-detail/game-center-team-detail.component.ts index a164ce831..c28a4343c 100644 --- a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-team-detail/game-center-team-detail.component.ts +++ b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-team-detail/game-center-team-detail.component.ts @@ -8,6 +8,8 @@ import { fa } from '@/services/font-awesome.service'; import { GameCenterTeamsResultsTeam } from '../game-center.models'; import { AdminService } from '@/api/admin.service'; import { ToastService } from '@/utility/services/toast.service'; +import { TeamService } from '@/api/team.service'; +import { SimpleEntity } from '@/api/models'; @Component({ selector: 'app-game-center-team-detail', @@ -17,8 +19,8 @@ import { ToastService } from '@/utility/services/toast.service'; export class GameCenterTeamDetailComponent implements OnInit { game!: { id: string; + maxTeamSize: number; name: string; - isTeamGame: boolean; }; team!: GameCenterTeamsResultsTeam; @@ -45,6 +47,7 @@ export class GameCenterTeamDetailComponent implements OnInit { private adminService: AdminService, private modalService: ModalConfirmService, private playerService: PlayerService, + private teamService: TeamService, private toastService: ToastService) { } public ngOnInit() { @@ -78,8 +81,8 @@ export class GameCenterTeamDetailComponent implements OnInit { extensionInMinutes: 30, game: { id: gameId, + maxTeamSize: this.game.maxTeamSize, name: this.game.name, - isTeamGame: this.game.isTeamGame }, teamIds: [this.team.id] }, @@ -118,4 +121,18 @@ export class GameCenterTeamDetailComponent implements OnInit { this.toastService.showMessage(`This player's name has been changed to **${args.name}**.${args.revisionReason ? ` (reason: **${args.revisionReason}**)` : ""}`); } + + protected async handleRemovePlayerConfirm(player: SimpleEntity) { + this.modalService.openConfirm({ + bodyContent: `Are you sure you want to remove **${player.name}** from team **${this.team.name}**?`, + title: "Remove Player From Team", + subtitle: player.name, + renderBodyAsMarkdown: true, + onConfirm: async () => { + const removedPlayer = await this.teamService.removePlayer(player.id, this.team.id); + this.team.players = this.team.players.filter(p => p.id !== player.id); + this.toastService.showMessage(`Player **${removedPlayer.player.name}** was removed from the team. _(They're still registered for **${removedPlayer.game.name}**.)_`); + } + }); + } } diff --git a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-teams/game-center-teams.component.html b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-teams/game-center-teams.component.html index 800d1eaa3..017f912cb 100644 --- a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-teams/game-center-teams.component.html +++ b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-teams/game-center-teams.component.html @@ -1,4 +1,10 @@ - + +
      +
    • {{ errors }}
    • +
    +
    + + There are {{results?.namesPendingApproval}} {{ "player" | pluralizer:results?.namesPendingApproval}} in this game who have pending name change requests. Click here @@ -11,7 +17,8 @@ Select All {{ (game.isTeamGame ? "Team" : "Player") | pluralizer }}
    - Rerank
    -
    +
    Sort by name +
    - - - +
    + + Your filters may be hiding some {{ this.game?.isTeamGame ? "teams" : "players" }}. Click here to clear them. +
    @@ -145,8 +153,9 @@
    -
    diff --git a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-teams/game-center-teams.component.ts b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-teams/game-center-teams.component.ts index 9f6ee23db..5ac5caeac 100644 --- a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-teams/game-center-teams.component.ts +++ b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-teams/game-center-teams.component.ts @@ -38,8 +38,10 @@ interface GameCenterTeamsFilterSettings { export class GameCenterTeamsComponent implements OnInit { @Input() gameId?: string; + protected errors: string[] = []; protected fa = fa; protected game?: Game; + protected hasFilters = false; protected isLoading = false; protected results?: GameCenterTeamsResults; protected selectedTeamIds: string[] = []; @@ -80,6 +82,11 @@ export class GameCenterTeamsComponent implements OnInit { return; } } + }), + this.teamService.teamRosterChanged$.subscribe(async teamId => { + if (this?.results?.teams?.items?.find(t => t.id === teamId)) { + await this.load(this.game?.id); + } }) ); } @@ -89,10 +96,7 @@ export class GameCenterTeamsComponent implements OnInit { } protected async handleClearAllFilters() { - this.filterSettings.advancement = undefined; - this.filterSettings.searchTerm = ""; - this.filterSettings.sort = "rank"; - this.filterSettings.sessionStatus = undefined; + this.filterSettings = { sort: "rank" }; await this.load(this.game?.id); } @@ -134,24 +138,16 @@ export class GameCenterTeamsComponent implements OnInit { return; } - await this.handleDeployGameResources(); + await this.handleDeployGameResources(eligibleTeams); } - private async handleDeployGameResources() { - const teams = this.resolveSelectedTeams(); + protected handleContextMenuError(errs: string[]) { + this.errors.push(...errs); + } - this.modalService.openConfirm({ - // bodyContent: `Are you sure you want to deploy resources for ${validTeamIds.length} teams?${appendInvalidTeamsClause}`, - bodyContent: `Are you sure you want to deploy resources for ${teams.length} team(s)?`, - onConfirm: async () => { - await this.gameService.deployResources(this.gameId!, teams.map(t => t.id)); - this.toastService.showMessage(`Deploying resources for **${teams.length} ${this.game?.isTeamGame ? "team" : "player"}(s)**.`); - this.selectedTeamIds = []; - }, - renderBodyAsMarkdown: true, - subtitle: this.game?.name, - title: "Deploy game resources" - }); + private async handleDeployGameResources(eligibleTeams: GameCenterTeamsResultsTeam[]) { + this.toastService.showMessage(`Deploying resources for ${eligibleTeams.length}...`); + await this.gameService.deployResources(this.gameId!, eligibleTeams.map(t => t.id)); } protected async handleExportCsvData(selectedTeamIds: string[]) { @@ -303,5 +299,12 @@ export class GameCenterTeamsComponent implements OnInit { private updateFilterConfig() { this.localStorageClient.add(StorageKey.GameCenterTeamsFilterSettings, this.filterSettings); + this.hasFilters = this.filterSettings && ( + !!this.filterSettings.advancement || + !!this.filterSettings.hasPendingNames || + !!this.filterSettings.searchTerm || + !!this.filterSettings.sessionStatus || + this.filterSettings?.sort !== 'rank' + ); } } 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 index 0d435c984..801f7a3a6 100644 --- 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 @@ -80,7 +80,7 @@

  • + *ngTemplateOutlet="tabItem; context: { $implicit: { tabKey: 'tickets', routerLink: 'tickets', label: 'Tickets' + (ctx.totalTicketCount ? ' (' + ctx.totalTicketCount + ')' : '') } }">
  • diff --git a/projects/gameboard-ui/src/app/admin/components/game-center/game-center.models.ts b/projects/gameboard-ui/src/app/admin/components/game-center/game-center.models.ts index c15b540f1..c2b87c3fe 100644 --- a/projects/gameboard-ui/src/app/admin/components/game-center/game-center.models.ts +++ b/projects/gameboard-ui/src/app/admin/components/game-center/game-center.models.ts @@ -46,6 +46,7 @@ export interface GameCenterContext { challengeCount: number; openTicketCount: number; + totalTicketCount: number; pointsAvailable: number; } diff --git a/projects/gameboard-ui/src/app/admin/components/team-admin-context-menu/team-admin-context-menu.component.html b/projects/gameboard-ui/src/app/admin/components/team-admin-context-menu/team-admin-context-menu.component.html deleted file mode 100644 index 04afadc10..000000000 --- a/projects/gameboard-ui/src/app/admin/components/team-admin-context-menu/team-admin-context-menu.component.html +++ /dev/null @@ -1,77 +0,0 @@ -
    - - -
    - - - - - -
  • - -
  • -
    diff --git a/projects/gameboard-ui/src/app/admin/components/team-admin-context-menu/team-admin-context-menu.component.scss b/projects/gameboard-ui/src/app/admin/components/team-admin-context-menu/team-admin-context-menu.component.scss deleted file mode 100644 index 2acaeeeb0..000000000 --- a/projects/gameboard-ui/src/app/admin/components/team-admin-context-menu/team-admin-context-menu.component.scss +++ /dev/null @@ -1,14 +0,0 @@ -@import "../../../../scss/variables"; - -.ctx-menu-button { - aspect-ratio: 1 / 1; -} - -.btn-danger { - color: red; - font-weight: bold; -} - -.btn-warning { - color: $warning; -} diff --git a/projects/gameboard-ui/src/app/admin/components/team-admin-context-menu/team-admin-context-menu.component.ts b/projects/gameboard-ui/src/app/admin/components/team-admin-context-menu/team-admin-context-menu.component.ts deleted file mode 100644 index c44942eb4..000000000 --- a/projects/gameboard-ui/src/app/admin/components/team-admin-context-menu/team-admin-context-menu.component.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { fa } from '@/services/font-awesome.service'; -import { ClipboardService } from '@/utility/services/clipboard.service'; -import { ToastService } from '@/utility/services/toast.service'; -import { SyncStartService } from '@/services/sync-start.service'; -import { firstValueFrom } from 'rxjs'; -import { PlayerService } from '@/api/player.service'; -import { TeamSessionResetType } from '@/api/teams.models'; -import { GameService } from '@/api/game.service'; -import { DateTime } from 'luxon'; -import { ModalConfirmService } from '@/services/modal-confirm.service'; -import { ManageManualChallengeBonusesModalComponent } from '../manage-manual-challenge-bonuses-modal/manage-manual-challenge-bonuses-modal.component'; -import { NowService } from '@/services/now.service'; -import { GameSessionService } from '@/services/game-session.service'; -import { TeamService } from '@/api/team.service'; - -export interface TeamAdminContextMenuSessionResetRequest { - team: TeamAdminContextMenuTeam; - resetType: TeamSessionResetType; -} - -export interface TeamAdminContextMenuTeam { - id: string; - gameId: string; - name: string; - isReady: boolean; - isTeamGame: boolean; - session: { start: DateTime | null, end: DateTime | null }; - captainPlayerId: string; -} - -@Component({ - selector: 'app-team-admin-context-menu', - templateUrl: './team-admin-context-menu.component.html', - styleUrls: ['./team-admin-context-menu.component.scss'] -}) -export class TeamAdminContextMenuComponent implements OnInit { - @Input() team?: TeamAdminContextMenuTeam; - @Input() isLegacy = false; - @Input() isSyncStartGame = false; - - @Output() teamUpdated = new EventEmitter(); - - protected fa = fa; - protected hasStartedSession = false; - - constructor( - private clipboardService: ClipboardService, - private gameService: GameService, - private gameSessionService: GameSessionService, - private modalService: ModalConfirmService, - private nowService: NowService, - private playerService: PlayerService, - private syncStartService: SyncStartService, - private teamService: TeamService, - private toastService: ToastService) { } - - ngOnInit(): void { - if (!this.team) - throw new Error("No player provided"); - - this.hasStartedSession = !!this.team.session?.start && this.team.session.start >= this.nowService.nowToDateTime(); - } - - protected confirmReset(request: TeamAdminContextMenuSessionResetRequest) { - // this is all really window-dressing around the "reset" operation, which we describe as an unenroll if the team's session hasn't started - const isUnenroll = request.resetType === "unenrollAndArchiveChallenges" && this.gameSessionService.canUnenrollSessionWithDateTime(request.team.session); - let confirmMessage = `Are you sure you want to unenroll ${this.team?.isTeamGame ? " team" : ""} **${request.team.name}** from the game?`; - let confirmTitle = `Unenroll ${request.team.name}?`; - let confirmToast = `**${request.team.name}** has been unenrolled.`; - - if (!isUnenroll) { - confirmMessage = `Are you sure you want to reset the session for ${this.team?.isTeamGame ? " team" : ""} **${request.team.name}**?`; - confirmTitle = `Reset ${request.team.name}'s session?`; - confirmToast = `${this.team?.isTeamGame ? "Team " : ""}**${request.team.name}**'s session has been reset.`; - - // accommodate various "types" of reset that can happen (e.g. keep challenges, don't keep challenges, destroy the universe and the unenroll) - if (request.resetType === "preserveChallenges") - confirmMessage += " Their challenges **won't** be archived automatically, and they'll remain enrolled in the game."; - else if (request.resetType == "unenrollAndArchiveChallenges") - confirmMessage += " Their challenges will be archived, and they'll be unenrolled from the game."; - } - - this.modalService.openConfirm({ - bodyContent: confirmMessage, - renderBodyAsMarkdown: true, - title: confirmTitle, - onConfirm: () => { - this.resetSession(request); - this.toastService.showMessage(confirmToast); - this.teamUpdated.emit(request.team); - }, - confirmButtonText: "Yes, reset", - cancelButtonText: "No, don't reset" - }); - } - - async copy(text: string, description: string) { - await this.clipboardService.copy(text); - this.toastService.showMessage(`Copied ${description} **${text}** to your clipboard.`); - } - - async handleDeployResources(team: TeamAdminContextMenuTeam) { - await this.gameService.deployResources(team.gameId, [team.id]); - this.toastService.showMessage(`Resources are being deployed for **${team.name}**.`); - } - - protected handleManageManualBonuses(team: TeamAdminContextMenuTeam) { - this.modalService.openComponent({ - content: ManageManualChallengeBonusesModalComponent, - context: { - teamId: team.id, - - }, - modalClasses: ["modal-xl"] - }); - } - - protected async handleSessionResetRequest(request: { team: { id: string }; type: TeamSessionResetType }) { - await firstValueFrom(this.teamService.resetSession({ teamId: request.team.id, resetType: request.type })); - } - - async handleUpdatePlayerReady(team: TeamAdminContextMenuTeam, isReady: boolean) { - await firstValueFrom(this.syncStartService.updateTeamReadyState(team.id, { isReady })); - this.teamUpdated.emit(team); - this.toastService.showMessage( - isReady ? - `**${team.name}**'s team has been readied.` : - `**${team.name}**'s team is no longer ready.` - ); - } - - private async resetSession(request: TeamAdminContextMenuSessionResetRequest): Promise { - await firstValueFrom(this.teamService.resetSession({ teamId: request.team.id, resetType: request.resetType })); - } - - async handleStartSession(team: TeamAdminContextMenuTeam) { - await firstValueFrom(this.playerService.startPlayerId(team.captainPlayerId)); - this.teamUpdated.emit(team); - this.toastService.showMessage(`Session started for ${team.name}`); - } -} 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 1c7ada80a..a18efcc06 100644 --- a/projects/gameboard-ui/src/app/admin/dashboard/dashboard.component.html +++ b/projects/gameboard-ui/src/app/admin/dashboard/dashboard.component.html @@ -224,7 +224,9 @@

    - +
    + Loading games... +
    diff --git a/projects/gameboard-ui/src/app/admin/user-api-keys/user-api-keys.component.html b/projects/gameboard-ui/src/app/admin/user-api-keys/user-api-keys.component.html index c7ea353ff..0e6f6b986 100644 --- a/projects/gameboard-ui/src/app/admin/user-api-keys/user-api-keys.component.html +++ b/projects/gameboard-ui/src/app/admin/user-api-keys/user-api-keys.component.html @@ -3,7 +3,7 @@

    - This user doesn't have any API keys. You can add one using the form below. + This user doesn't have any API keys. You can add one using the form below.

    @@ -75,7 +75,7 @@
    You've got a new API key!
    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 c9a1841ae..d9edad887 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 @@ -11,7 +11,7 @@

    Users

    Search
    -
    @@ -133,13 +133,13 @@

    Users

    Name
    -
    - +
    - + Your name will be visible on the public scoreboard. It's also subject to approval - until an administrator has approved it, a randomly-generated name will be displayed instead. @@ -119,9 +119,9 @@

    Captains

    + placeholder="Generate a code →">
    - 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 01553ee2d..98aafdac7 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 @@ -15,6 +15,7 @@ import { fa } from '@/services/font-awesome.service'; import { ClipboardService } from '@/utility/services/clipboard.service'; import { ToastService } from '@/utility/services/toast.service'; import { GameRegistrationType } from '@/api/game-models'; +import { UserRolePermissionsService } from '@/api/user-role-permissions.service'; @Component({ selector: 'app-player-enroll', @@ -63,6 +64,7 @@ export class PlayerEnrollComponent implements OnInit, OnDestroy { private clipboard: ClipboardService, private hubService: NotificationService, private localUserService: LocalUserService, + private permissionsService: UserRolePermissionsService, private toastService: ToastService ) { this.ctx$ = timer(0, 1000).pipe( @@ -85,7 +87,7 @@ export class PlayerEnrollComponent implements OnInit, OnDestroy { tap(ctx => { const localUser = this.localUserService.user$.value; const hasPlayerSession = (!!ctx.player.id && !!ctx.player.session && !ctx.player.session.isBefore); - this.canAdminEnroll = !!this.localUserService.can('Play_IgnoreExecutionWindow') && !hasPlayerSession; + this.canAdminEnroll = !!this.permissionsService.can('Play_IgnoreExecutionWindow') && !hasPlayerSession; this.canStandardEnroll = !!localUser && !hasPlayerSession && ctx.game.registrationType != "none" && @@ -163,6 +165,7 @@ export class PlayerEnrollComponent implements OnInit, OnDestroy { this.ctx.player = { ...this.ctx.player, name: p.name, + nameStatus: p.nameStatus, approvedName: p.approvedName }; } diff --git a/projects/gameboard-ui/src/app/game/player-presence/player-presence.component.ts b/projects/gameboard-ui/src/app/game/player-presence/player-presence.component.ts index fe5290fce..a141ca9e6 100644 --- a/projects/gameboard-ui/src/app/game/player-presence/player-presence.component.ts +++ b/projects/gameboard-ui/src/app/game/player-presence/player-presence.component.ts @@ -10,7 +10,6 @@ import { PlayerService } from '../../api/player.service'; import { NotificationService } from '../../services/notification.service'; import { GameHubService } from '../../services/signalR/game-hub.service'; import { SyncStartService } from '../../services/sync-start.service'; -import { HubConnectionState } from '@microsoft/signalr'; import { LogService } from '../../services/log.service'; import { SponsorService } from '@/api/sponsor.service'; @@ -55,11 +54,6 @@ export class PlayerPresenceComponent implements OnInit { ]).pipe( map(combo => ({ hubState: combo[0], actors: combo[1], player: combo[2], syncStartState: combo[3] })), map(context => { - if (!context.hubState || context.hubState.connectionState == HubConnectionState.Disconnected) { - this.log.logWarning("Can't render player presence component: SignalR hub is disconnected."); - return null; - } - if (!context.player) { this.log.logWarning("Can't render player presence component: the context has no Player object."); return null; @@ -107,18 +101,12 @@ export class PlayerPresenceComponent implements OnInit { } protected promoteToManager(localPlayer: Player, playerId: string) { - this.hub.state$.pipe(first()).subscribe(s => { - 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."); - } + if (!localPlayer) { + throw new Error("Can't resolve the current player to promote manager."); + } - this.playerApi.promoteToCaptain(s.id, playerId, { currentCaptainId: localPlayer.id }).pipe(first()).subscribe(_ => { - this.onManagerPromoted.emit(playerId); - }); + this.playerApi.promoteToCaptain(localPlayer.teamId, playerId, { currentCaptainId: localPlayer.id }).pipe(first()).subscribe(_ => { + this.onManagerPromoted.emit(playerId); }); } } diff --git a/projects/gameboard-ui/src/app/game/player-session/player-session.component.html b/projects/gameboard-ui/src/app/game/player-session/player-session.component.html index 60785e877..22068e13f 100644 --- a/projects/gameboard-ui/src/app/game/player-session/player-session.component.html +++ b/projects/gameboard-ui/src/app/game/player-session/player-session.component.html @@ -17,16 +17,16 @@
    -
    +
    - Session ended - + Session ended + Time Remaining:
    + [class]="'ml-2 fw-bold ' + (((timeRemainingMs$ | async) || 0) | countdowncolor)"> {{ ((timeRemainingMs$ | async) || 0) | countdown }}
    diff --git a/projects/gameboard-ui/src/app/game/player-session/player-session.component.ts b/projects/gameboard-ui/src/app/game/player-session/player-session.component.ts index 6c2ac07c5..98f163071 100644 --- a/projects/gameboard-ui/src/app/game/player-session/player-session.component.ts +++ b/projects/gameboard-ui/src/app/game/player-session/player-session.component.ts @@ -7,12 +7,11 @@ import { map, tap } from 'rxjs/operators'; import { GameContext } from '@/api/game-models'; import { Player } from '../../api/player-models'; import { PlayerService } from '../../api/player.service'; -import { UserService } from '@/api/user.service'; -import { UserService as LocalUserService } from "@/utility/user.service"; import { fa } from '@/services/font-awesome.service'; import { ModalConfirmConfig } from '@/core/components/modal/modal.models'; import { ModalConfirmService } from '@/services/modal-confirm.service'; import { TeamService } from '@/api/team.service'; +import { UserRolePermissionsService } from '@/api/user-role-permissions.service'; @Component({ selector: 'app-player-session', @@ -37,16 +36,15 @@ export class PlayerSessionComponent implements OnDestroy { protected isDoubleChecking = false; protected canAdminStart = false; - protected canIgnoreSessionResetSettings$ = this.localUserService.can$("Play_IgnoreSessionResetSettings"); + protected canIgnoreSessionResetSettings$ = this.permissionsService.can$("Play_IgnoreSessionResetSettings"); protected hasTimeRemaining = false; protected timeRemainingMs$?: Observable; constructor( private api: PlayerService, private modalService: ModalConfirmService, - private localUserService: LocalUserService, + private permissionsService: UserRolePermissionsService, private teamService: TeamService, - private userService: UserService, ) { } async ngOnInit() { @@ -71,7 +69,7 @@ export class PlayerSessionComponent implements OnDestroy { } }), tap(ctx => { - this.canAdminStart = this.localUserService.can('Play_IgnoreExecutionWindow'); + this.canAdminStart = this.permissionsService.can('Play_IgnoreExecutionWindow'); }), // set up countdown tap(ctx => { diff --git a/projects/gameboard-ui/src/app/guards/game-is-started.guard.ts b/projects/gameboard-ui/src/app/guards/game-is-started.guard.ts index 714c5bfab..362cc0994 100644 --- a/projects/gameboard-ui/src/app/guards/game-is-started.guard.ts +++ b/projects/gameboard-ui/src/app/guards/game-is-started.guard.ts @@ -2,6 +2,7 @@ import { GamePlayState } from '@/api/game-models'; import { GameService } from '@/api/game.service'; import { PlayerService } from '@/api/player.service'; import { TeamService } from '@/api/team.service'; +import { UserRolePermissionsService } from '@/api/user-role-permissions.service'; import { LogService } from '@/services/log.service'; import { UserService } from '@/utility/user.service'; import { Injectable } from '@angular/core'; @@ -13,6 +14,7 @@ export class GameIsStarted implements CanActivate, CanActivateChild { constructor( private localUserService: UserService, private log: LogService, + private permissionsService: UserRolePermissionsService, private playerService: PlayerService, private teamService: TeamService ) { } @@ -31,7 +33,7 @@ export class GameIsStarted implements CanActivate, CanActivateChild { // if the user is admin/tester, they can ignore start phase restrictions const localUser = this.localUserService.user$.getValue(); - if (await firstValueFrom(this.localUserService.can$("Play_IgnoreExecutionWindow"))) { + if (await firstValueFrom(this.permissionsService.can$("Play_IgnoreExecutionWindow"))) { return true; } diff --git a/projects/gameboard-ui/src/app/guards/user-is-playing.guard.ts b/projects/gameboard-ui/src/app/guards/user-is-playing.guard.ts index af44cd033..2da10441f 100644 --- a/projects/gameboard-ui/src/app/guards/user-is-playing.guard.ts +++ b/projects/gameboard-ui/src/app/guards/user-is-playing.guard.ts @@ -25,7 +25,7 @@ export class UserIsPlayingGuard implements CanActivate, CanActivateChild { private async _canActivate(route: ActivatedRouteSnapshot) { const gameId = route.paramMap.get('gameId') || ''; - const playerId = route.paramMap.get('playerId' || ''); + const playerId = route.paramMap.get('playerId') || ''; this.log.logInfo("Resolving UserIsPlayingGuard", gameId, playerId); // need either a game or a player to decide diff --git a/projects/gameboard-ui/src/app/home/oidc/oidc.component.html b/projects/gameboard-ui/src/app/home/oidc/oidc.component.html index 138057386..3294d2125 100644 --- a/projects/gameboard-ui/src/app/home/oidc/oidc.component.html +++ b/projects/gameboard-ui/src/app/home/oidc/oidc.component.html @@ -1,5 +1,5 @@ -
    Validating authentication token...
    +
    Logging you in...
    {{message}}
    diff --git a/projects/gameboard-ui/src/app/prac/components/practice-challenge-list/practice-challenge-list.component.html b/projects/gameboard-ui/src/app/prac/components/practice-challenge-list/practice-challenge-list.component.html index 1c8b916e7..38255e960 100644 --- a/projects/gameboard-ui/src/app/prac/components/practice-challenge-list/practice-challenge-list.component.html +++ b/projects/gameboard-ui/src/app/prac/components/practice-challenge-list/practice-challenge-list.component.html @@ -82,11 +82,15 @@

    Need a place to start?

    - + [queryParams]="{ term: c.game.id }"> + -
    {{ c.gameName }}
    +
    + {{ c.game.name }} + + +
    @@ -116,6 +120,9 @@

    Need a place to start?

    [tooltip]="canPlayChallengesTooltip || ''"> {{challenge.name}}

    + + diff --git a/projects/gameboard-ui/src/app/prac/components/practice-challenge-list/practice-challenge-list.component.ts b/projects/gameboard-ui/src/app/prac/components/practice-challenge-list/practice-challenge-list.component.ts index c6d598570..79bd52087 100644 --- a/projects/gameboard-ui/src/app/prac/components/practice-challenge-list/practice-challenge-list.component.ts +++ b/projects/gameboard-ui/src/app/prac/components/practice-challenge-list/practice-challenge-list.component.ts @@ -1,9 +1,8 @@ import { Component } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { faSearch } from '@fortawesome/free-solid-svg-icons'; -import { BehaviorSubject, Observable, combineLatest, firstValueFrom, map, switchMap, tap } from 'rxjs'; +import { Observable, combineLatest, firstValueFrom, map, switchMap, tap } from 'rxjs'; import { Search } from '@/api/models'; -import { SpecSummary } from '@/api/spec-models'; import { PracticeService } from '@/services/practice.service'; import { RouterService } from '@/services/router.service'; import { UserService as LocalUserService } from "@/utility/user.service"; @@ -11,6 +10,8 @@ import { slug } from '@/../tools/functions'; import { ApiUser } from '@/api/user-models'; import { UnsubscriberService } from '@/services/unsubscriber.service'; import { AuthService } from '@/utility/auth.service'; +import { PracticeChallengeView } from '@/prac/practice.models'; +import { fa } from '@/services/font-awesome.service'; @Component({ selector: 'app-practice-challenge-list', @@ -19,12 +20,13 @@ import { AuthService } from '@/utility/auth.service'; providers: [UnsubscriberService] }) export class PracticeChallengeListComponent { - list$: Observable; + list$: Observable; appname = ''; faSearch = faSearch; protected canPlayChallenges = false; protected canPlayChallengesTooltip: string | null = null; + protected fa = fa; protected hasSponsor$: Observable; protected introTextMarkdown = ""; protected localUser$: Observable; diff --git a/projects/gameboard-ui/src/app/prac/components/practice-session/practice-session.component.html b/projects/gameboard-ui/src/app/prac/components/practice-session/practice-session.component.html index 0bfddebb3..7ab1766d5 100644 --- a/projects/gameboard-ui/src/app/prac/components/practice-session/practice-session.component.html +++ b/projects/gameboard-ui/src/app/prac/components/practice-session/practice-session.component.html @@ -19,7 +19,7 @@
    + (confirm)="play({ id: spec.id, game: { id: spec.game.id }} )" class="d-block"> Start Practice Session diff --git a/projects/gameboard-ui/src/app/prac/components/practice-session/practice-session.component.ts b/projects/gameboard-ui/src/app/prac/components/practice-session/practice-session.component.ts index 7f3b4b221..7db9f8b1b 100644 --- a/projects/gameboard-ui/src/app/prac/components/practice-session/practice-session.component.ts +++ b/projects/gameboard-ui/src/app/prac/components/practice-session/practice-session.component.ts @@ -16,7 +16,7 @@ import { PracticeChallengeSolvedModalComponent } from '../practice-challenge-sol import { TeamService } from '@/api/team.service'; import { WindowService } from '@/services/window.service'; import { UserActiveChallenge } from '@/api/challenges.models'; -import { PracticeSession } from '@/prac/practice.models'; +import { PracticeChallengeView, PracticeSession } from '@/prac/practice.models'; @Component({ selector: 'app-practice-session', @@ -28,7 +28,7 @@ import { PracticeSession } from '@/prac/practice.models'; }) export class PracticeSessionComponent implements OnInit { protected windowWidth$ = this.windowService.resize$; - spec$: Observable; + spec$: Observable; authed$: Observable; protected errors: any = []; @@ -58,7 +58,7 @@ export class PracticeSessionComponent implements OnInit { map(p => p.specId), distinctUntilChanged(), switchMap(p => practiceService.searchChallenges({ term: p })), - map(r => !r.results.items.length ? ({ name: "Not Found" } as SpecSummary) : r.results.items[0]), + map(r => !r.results.items.length ? ({ name: "Not Found" } as PracticeChallengeView) : r.results.items[0]), ); this.unsub.add(this.activeChallengesRepo.challengeCompleted$.subscribe(c => this.handleActiveChallengeCompleted(c))); @@ -105,7 +105,7 @@ export class PracticeSessionComponent implements OnInit { this.isStartingSession = false; } - protected handleStartNewChallengeClick(spec: SpecSummary) { + protected handleStartNewChallengeClick(spec: PracticeChallengeView) { const currentPracticeChallenge = this.activeChallengesRepo.getActivePracticeChallenge(); if (!currentPracticeChallenge) { throw new Error("Can't end previous challenge and start a new one - no previous challenge detected."); @@ -117,7 +117,7 @@ export class PracticeSessionComponent implements OnInit { renderBodyAsMarkdown: true, onConfirm: async () => { await this.teamService.endSession({ teamId: currentPracticeChallenge.team.id }); - this.play({ id: spec.id, game: { id: spec.gameId } }); + this.play({ id: spec.id, game: { id: spec.game.id } }); } }); } diff --git a/projects/gameboard-ui/src/app/prac/prac.module.ts b/projects/gameboard-ui/src/app/prac/prac.module.ts index 8f8820b9b..05bcbcfd1 100644 --- a/projects/gameboard-ui/src/app/prac/prac.module.ts +++ b/projects/gameboard-ui/src/app/prac/prac.module.ts @@ -19,6 +19,7 @@ import { ErrorDivComponent } from '@/standalone/core/components/error-div/error- import { SpinnerComponent } from '@/standalone/core/components/spinner/spinner.component'; import { EpochMsToTimeRemainingStringPipe } from '@/standalone/core/pipes/epoch-ms-to-time-remaining.pipe'; import { EpochMsToMsRemainingPipe } from '@/standalone/core/pipes/epoch-ms-to-ms-remaining.pipe'; +import { InfoBubbleComponent } from '@/standalone/core/components/info-bubble/info-bubble.component'; @NgModule({ declarations: [ @@ -50,6 +51,7 @@ import { EpochMsToMsRemainingPipe } from '@/standalone/core/pipes/epoch-ms-to-ms EpochMsToMsRemainingPipe, EpochMsToTimeRemainingStringPipe, ErrorDivComponent, + InfoBubbleComponent, PlayComponent, SpinnerComponent ] diff --git a/projects/gameboard-ui/src/app/prac/practice.models.ts b/projects/gameboard-ui/src/app/prac/practice.models.ts index 30c8dfaff..989e9cdd0 100644 --- a/projects/gameboard-ui/src/app/prac/practice.models.ts +++ b/projects/gameboard-ui/src/app/prac/practice.models.ts @@ -1,9 +1,8 @@ import { GameCardContext } from "@/api/game-models"; -import { PagedArray, SimpleEntity, TimestampRange } from "@/api/models"; -import { SpecSummary } from "@/api/spec-models"; +import { PagedArray, TimestampRange } from "@/api/models"; export interface SearchPracticeChallengesResult { - results: PagedArray; + results: PagedArray; } export interface SearchGamesResult { @@ -27,3 +26,20 @@ export interface PracticeSession { teamId: string; userId: string; } + +export interface PracticeChallengeView { + id: string; + name: string; + description: string; + text: string; + averageDeploySeconds: number; + isHidden: boolean; + solutionGuideUrl: string; + tags: string[]; + game: { + id: string; + name: string; + logo: string; + isHidden: string; + } +} diff --git a/projects/gameboard-ui/src/app/services/api-date-time.service.ts b/projects/gameboard-ui/src/app/services/api-date-time.service.ts index 1bbffa221..6427c4d58 100644 --- a/projects/gameboard-ui/src/app/services/api-date-time.service.ts +++ b/projects/gameboard-ui/src/app/services/api-date-time.service.ts @@ -3,8 +3,8 @@ import { DateTime } from 'luxon'; @Injectable({ providedIn: 'root' }) export class ApiDateTimeService { - toDateTime(input?: string): DateTime | null { - if (!input || input === "undefine") return null; + toDateTime(input?: string | Date): DateTime | null { + if (!input || input === "undefined") return null; // as of now, the API returns date objects as string (blargh) const parsedDateTime = DateTime.fromJSDate(new Date(input)); diff --git a/projects/gameboard-ui/src/app/services/app-notifications.service.ts b/projects/gameboard-ui/src/app/services/app-notifications.service.ts index 8f0319635..b71afb6dc 100644 --- a/projects/gameboard-ui/src/app/services/app-notifications.service.ts +++ b/projects/gameboard-ui/src/app/services/app-notifications.service.ts @@ -1,6 +1,7 @@ -import { ToastService } from '@/utility/services/toast.service'; import { Injectable, OnDestroy } from '@angular/core'; +import { Router, UrlTree } from '@angular/router'; import { BehaviorSubject, Observable, Subject, Subscription, firstValueFrom, from, groupBy, map, mergeMap, tap, throttleTime } from 'rxjs'; +import { ToastService } from '@/utility/services/toast.service'; import { LogService } from './log.service'; import { WindowService } from './window.service'; import { ConfigService } from '@/utility/config.service'; @@ -11,8 +12,7 @@ export type CanUseBrowserNotificationsResult = "denied" | "pending" | "unsupport export interface SendAppNotification { title: string, body: string, - allowRenotify?: boolean, - appUrl?: string, + appUrl?: string | UrlTree, tag?: string } @@ -26,6 +26,7 @@ export class AppNotificationsService implements OnDestroy { constructor( private config: ConfigService, private log: LogService, + private router: Router, private toastService: ToastService, private userService: UserService, private windowService: WindowService) { @@ -68,7 +69,10 @@ export class AppNotificationsService implements OnDestroy { private async onAppNotificationSend(sendNotification: SendAppNotification) { if (this._canShowBrowserNotifications$.value !== "allowed") { this.log.logWarning(`Can't send browser notification (${this._canShowBrowserNotifications$.value}) - falling back to toast.`); - this.toastService.showMessage(`${sendNotification.title}: ${sendNotification.body}`); + this.toastService.show({ + text: `${sendNotification.title}: ${sendNotification.body}`, + onClick: sendNotification.appUrl ? () => this.router.navigateByUrl(sendNotification.appUrl!) : undefined, + }); return; } @@ -80,7 +84,6 @@ export class AppNotificationsService implements OnDestroy { const notification = new Notification(sendNotification.title, { body: sendNotification.body, tag: sendNotification.tag, - renotify: !!sendNotification.allowRenotify, }); // play audio notification (we only do this with the OS-level notification, because otherwise it's hard to know why the sound happened if you're diff --git a/projects/gameboard-ui/src/app/services/font-awesome.service.ts b/projects/gameboard-ui/src/app/services/font-awesome.service.ts index 860f13753..51fc3c93d 100644 --- a/projects/gameboard-ui/src/app/services/font-awesome.service.ts +++ b/projects/gameboard-ui/src/app/services/font-awesome.service.ts @@ -3,6 +3,7 @@ import { SafeHtml } from '@angular/platform-browser'; import { IconDefinition, faArrowLeft, + faArrowsSpin, faArrowUp, faBars, faBarsStaggered, @@ -70,7 +71,6 @@ import { faUser, faUsers, faXmark, - faArrowsSpin } from '@fortawesome/free-solid-svg-icons'; import { faOpenid } from "@fortawesome/free-brands-svg-icons"; diff --git a/projects/gameboard-ui/src/app/services/local-storage.service.ts b/projects/gameboard-ui/src/app/services/local-storage.service.ts index 41d4154bb..89cf5a349 100644 --- a/projects/gameboard-ui/src/app/services/local-storage.service.ts +++ b/projects/gameboard-ui/src/app/services/local-storage.service.ts @@ -5,7 +5,7 @@ export enum StorageKey { ExternalGameUrl = "gameServerUrl", GameCenterTeamsFilterSettings = "gameCenterTeamsFilterSettings", Gameboard = "gameboard", - UsePlayPane = "usePlayPane" + UseStickyChallengePanel = "usePlayPane" } @Injectable({ providedIn: 'root' }) diff --git a/projects/gameboard-ui/src/app/services/signalR/support-hub.service.ts b/projects/gameboard-ui/src/app/services/signalR/support-hub.service.ts index c05474ae4..3d2acc941 100644 --- a/projects/gameboard-ui/src/app/services/signalR/support-hub.service.ts +++ b/projects/gameboard-ui/src/app/services/signalR/support-hub.service.ts @@ -7,6 +7,7 @@ import { ConfigService } from '@/utility/config.service'; import { UserService } from '@/api/user.service'; import { HubConnectionState } from '@microsoft/signalr'; import { Observable } from 'rxjs'; +import { RouterService } from '../router.service'; @Injectable({ providedIn: 'root' }) export class SupportHubService { @@ -17,6 +18,7 @@ export class SupportHubService { configService: ConfigService, userService: UserService, private appNotificationsService: AppNotificationsService, + private routerService: RouterService, private logService: LogService ) { this._signalRService = new SignalRService(configService, logService, userService); @@ -47,7 +49,7 @@ export class SupportHubService { this.appNotificationsService.send({ title: `Ticket Closed: ${ev.data.ticket.key}`, body: ev.data.ticket.summary, - appUrl: `support/tickets/${ev.data.ticket.id}`, + appUrl: this.routerService.getTicketUrl(ev.data.ticket.id), tag: `support-closed-${ev.data.ticket.id}` }); } @@ -58,7 +60,7 @@ export class SupportHubService { this.appNotificationsService.send({ title: `New Ticket: ${ev.data.ticket.key}`, body: ev.data.ticket.summary, - appUrl: `support/tickets/${ev.data.ticket.id}`, + appUrl: this.routerService.getTicketUrl(ev.data.ticket.id), tag: `support-created-${ev.data.ticket.id}` }); } @@ -69,7 +71,7 @@ export class SupportHubService { this.appNotificationsService.send({ title: `Ticket updated by Support: ${ev.data.ticket.key}`, body: ev.data.ticket.summary, - appUrl: `support/tickets/${ev.data.ticket.id}`, + appUrl: this.routerService.getTicketUrl(ev.data.ticket.id), tag: `support-updated-by-support-${ev.data.ticket.id}` }); } @@ -80,7 +82,7 @@ export class SupportHubService { this.appNotificationsService.send({ title: `Ticket updated by Player: ${ev.data.ticket.key}`, body: ev.data.ticket.summary, - appUrl: `support/tickets/${ev.data.ticket.id}`, + appUrl: this.routerService.getTicketUrl(ev.data.ticket.id), tag: `support-updated-by-user-${ev.data.ticket.id}` }); } diff --git a/projects/gameboard-ui/src/app/sponsors/components/sponsor-with-children-picker/sponsor-with-children-picker.component.html b/projects/gameboard-ui/src/app/sponsors/components/sponsor-with-children-picker/sponsor-with-children-picker.component.html index 85448e1ea..74c44d19c 100644 --- a/projects/gameboard-ui/src/app/sponsors/components/sponsor-with-children-picker/sponsor-with-children-picker.component.html +++ b/projects/gameboard-ui/src/app/sponsors/components/sponsor-with-children-picker/sponsor-with-children-picker.component.html @@ -4,14 +4,16 @@

    {{ parentSponsor.name }}

    - - - - +
    + + + + - - - + + + +
    @@ -19,7 +21,7 @@

    Other Sponsors

    -
    {{sponsor.name || sponsor.id}}
    diff --git a/projects/gameboard-ui/src/app/standalone/core/components/info-bubble/info-bubble.component.scss b/projects/gameboard-ui/src/app/standalone/core/components/info-bubble/info-bubble.component.scss new file mode 100644 index 000000000..0bd59a5bd --- /dev/null +++ b/projects/gameboard-ui/src/app/standalone/core/components/info-bubble/info-bubble.component.scss @@ -0,0 +1,16 @@ +@import "../../../../../scss/variables"; + +.icon-container { + align-items: center; + aspect-ratio: 1/1; + justify-content: center; + background-color: $body-color; + border-radius: 50%; + display: flex; + height: 22px; + width: 22px; +} + +::ng-deep .info-bubble-component fa-icon svg path { + fill: $body-bg; +} diff --git a/projects/gameboard-ui/src/app/standalone/core/components/info-bubble/info-bubble.component.ts b/projects/gameboard-ui/src/app/standalone/core/components/info-bubble/info-bubble.component.ts new file mode 100644 index 000000000..3f321e684 --- /dev/null +++ b/projects/gameboard-ui/src/app/standalone/core/components/info-bubble/info-bubble.component.ts @@ -0,0 +1,22 @@ +import { Component, Input } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { IconDefinition } from '@fortawesome/fontawesome-svg-core'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { TooltipModule } from 'ngx-bootstrap/tooltip'; +import { fa } from '@/services/font-awesome.service'; + +@Component({ + selector: 'app-info-bubble', + standalone: true, + imports: [ + CommonModule, + FontAwesomeModule, + TooltipModule, + ], + styleUrls: ['./info-bubble.component.scss'], + template: `
    ` +}) +export class InfoBubbleComponent { + @Input() icon: IconDefinition = fa.infoCircle; + @Input() tooltipText?: string; +} diff --git a/projects/gameboard-ui/src/app/standalone/directives/if-has-permission.directive.ts b/projects/gameboard-ui/src/app/standalone/directives/if-has-permission.directive.ts index db1095f6d..33c2bb5b0 100644 --- a/projects/gameboard-ui/src/app/standalone/directives/if-has-permission.directive.ts +++ b/projects/gameboard-ui/src/app/standalone/directives/if-has-permission.directive.ts @@ -6,7 +6,7 @@ import { UserRolePermissionsService } from '@/api/user-role-permissions.service' import { ApiUser } from '@/api/user-models'; @Directive({ - providers: [UnsubscriberService, LocalUserService], + providers: [UnsubscriberService], selector: '[appIfHasPermission]', standalone: true }) @@ -39,7 +39,7 @@ export class IfHasPermissionDirective implements OnChanges { } private evaluate(user: ApiUser | null | undefined) { - const hasPermission = this.requiredPermission && user && this.permissionsService.can(user, this.requiredPermission); + const hasPermission = this.requiredPermission && user && this.permissionsService.canUser(user, this.requiredPermission); if (hasPermission && !this.hasView) { this.viewContainer.createEmbeddedView(this.templateRef); diff --git a/projects/gameboard-ui/src/app/standalone/games/components/challenge-questions/challenge-questions.component.ts b/projects/gameboard-ui/src/app/standalone/games/components/challenge-questions/challenge-questions.component.ts index df496a886..ce1830f8f 100644 --- a/projects/gameboard-ui/src/app/standalone/games/components/challenge-questions/challenge-questions.component.ts +++ b/projects/gameboard-ui/src/app/standalone/games/components/challenge-questions/challenge-questions.component.ts @@ -6,7 +6,6 @@ import { ChallengesService } from '@/api/challenges.service'; import { CoreModule } from '@/core/core.module'; import { fa } from '@/services/font-awesome.service'; import { SpinnerComponent } from '@/standalone/core/components/spinner/spinner.component'; -import { HoldConfirmButtonComponent } from "@/standalone/core/components/hold-confirm-button/hold-confirm-button.component"; import { ToastService } from '@/utility/services/toast.service'; import { AbstractControl, FormGroup } from '@angular/forms'; import { ChallengeQuestionsFormService } from '@/services/challenge-questions-form.service'; @@ -158,9 +157,9 @@ export class ChallengeQuestionsComponent implements OnChanges { }; }); - // select the "last" tab with unanswered questions - const selectIndex = this.progress.variant.sections.findIndex(s => s.score < s.scoreMax) || this.progress.variant.sections.length - 1; - this.handleSectionSelect(selectIndex); + // select the first section tab with unanswered questions + const unmaxedSectionIndex = this.progress.variant.sections.findIndex(s => s.score < s.scoreMax); + this.handleSectionSelect(unmaxedSectionIndex === -1 ? this.progress.variant.sections.length - 1 : unmaxedSectionIndex); // find out if any submissions have been made (decides the availability of the "submission history" modal) this.hasSubmissionHistory = this.progress.attempts > 0; diff --git a/projects/gameboard-ui/src/app/standalone/games/components/play/play.component.html b/projects/gameboard-ui/src/app/standalone/games/components/play/play.component.html index 721ae585f..09cd2da10 100644 --- a/projects/gameboard-ui/src/app/standalone/games/components/play/play.component.html +++ b/projects/gameboard-ui/src/app/standalone/games/components/play/play.component.html @@ -1,57 +1,36 @@ +
    - - + - - Stopping challenge resources... - - - - + +

    Check out the new challenge resources panel!

    Click here + (click)="setIsStickyPanelEnabled(!isStickyChallengePanelSelected)">Click here to "sticky" the Challenge panel so it's always visible as you scroll down the page. (You can return the panel to the bottom of the page at any time by clicking the appropriate link in the panel.)

    -

    +

    - +

    Challenge Consoles

    -
    -
      -
    • - -
    • -
    -
    - -
    - - - Destroy - - - - Deploy - -
    +

    Challenge Questions

    @@ -61,8 +40,8 @@

    Challenge Questions

    @@ -87,39 +66,18 @@

    Challenge Questions

    -
    - +
    +
    - - + +
    Challenge Consoles
    - -
    -
      -
    • - -
    • -
    -
    - -
    - - - Destroy - - - - Deploy - -
    +
    Questions
    @@ -150,19 +108,15 @@
    Need help?
    Click here + (click)="setIsStickyPanelEnabled(!isStickyChallengePanelSelected)">here to deactivate the sticky Challenge panel
    - - Loading your challenge... - - -

    Solution Guide

    +

    Solution Guide

    Having trouble? We've created a step-by-step solution guide for this challenge. If you get @@ -170,3 +124,60 @@

    Solution Guide

    + + +
    + + + + + + + + + + + + + + + + + +
    + + + Destroy + + + + Deploy + +
    +
    +
    +
    + + +
      +
    • + +
    • +
    +
    + + + Starting your challenge consoles... + + + + Stopping your challenge consoles... + + + +
    + Your challenge consoles are currently undeployed. Click Deploy to bring them back up. +
    +
    diff --git a/projects/gameboard-ui/src/app/standalone/games/components/play/play.component.ts b/projects/gameboard-ui/src/app/standalone/games/components/play/play.component.ts index 3def41e04..5f3fc001a 100644 --- a/projects/gameboard-ui/src/app/standalone/games/components/play/play.component.ts +++ b/projects/gameboard-ui/src/app/standalone/games/components/play/play.component.ts @@ -1,5 +1,6 @@ +import { CommonModule } from '@angular/common'; import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; -import { firstValueFrom, Observable, tap } from 'rxjs'; +import { BehaviorSubject, firstValueFrom, Observable, tap } from 'rxjs'; import { fa } from "@/services/font-awesome.service"; import { RouterService } from '@/services/router.service'; import { ChallengesService } from '@/api/challenges.service'; @@ -7,10 +8,8 @@ import { ChallengeSolutionGuide, UserActiveChallenge } from '@/api/challenges.mo import { PlayerMode } from '@/api/player-models'; import { ActiveChallengesRepo } from '@/stores/active-challenges.store'; import { UnsubscriberService } from '@/services/unsubscriber.service'; -import { SpecSummary } from '@/api/spec-models'; import { WindowService } from '@/services/window.service'; import { LocalStorageService, StorageKey } from '@/services/local-storage.service'; -import { CommonModule } from '@angular/common'; import { SpinnerComponent } from '@/standalone/core/components/spinner/spinner.component'; import { CoreModule } from '@/core/core.module'; import { ToSupportCodePipe } from '@/standalone/core/pipes/to-support-code.pipe'; @@ -21,6 +20,9 @@ import { ErrorDivComponent } from '@/standalone/core/components/error-div/error- import { VmLinkComponent } from "../vm-link/vm-link.component"; import { UserService } from '@/utility/user.service'; import { UserSettingsService } from '@/services/user-settings.service'; +import { PracticeChallengeView } from '@/prac/practice.models'; + +export type PlayChallengeDeployState = "deployed" | "deploying" | "undeploying" | "undeployed"; @Component({ selector: 'app-play', @@ -42,23 +44,23 @@ import { UserSettingsService } from '@/services/user-settings.service'; }) export class PlayComponent implements OnChanges { @Input() autoPlay = false; - @Input() challengeSpec: SpecSummary | null = null; + @Input() challengeSpec: PracticeChallengeView | null = null; @Input() playerId?: string; @Output() challengeStarted = new EventEmitter(); - @Output() deployStatusChanged = new EventEmitter(); + @Output() deployStateChange = new EventEmitter(); protected challenge: UserActiveChallenge | null = null; + protected deployState$ = new BehaviorSubject("undeployed"); protected errors: any[] = []; protected fa = fa; - protected isDeploying = false; - protected isMiniPlayerAvailable = false; - protected isMiniPlayerSelected = false; - protected isUndeploying = false; - protected showMiniPlayerPrompt = false; + protected isStickyChallengePanelAvailable = false; + protected isStickyChallengePanelSelected = false; + protected showStickyChallengePanelPrompt = false; protected solutionGuide: ChallengeSolutionGuide | null = null; protected vmUrls: { [id: string]: string } = {}; protected windowWidth$: Observable; + constructor( private activeChallengesRepo: ActiveChallengesRepo, private challengesService: ChallengesService, @@ -71,15 +73,20 @@ export class PlayComponent implements OnChanges { this.windowWidth$ = windowService.resize$; this.unsub.add( windowService.resize$.subscribe(width => { - this.isMiniPlayerAvailable = width >= 1140; - this.isMiniPlayerSelected = this.localStorage.get(StorageKey.UsePlayPane) === "true"; - this.showMiniPlayerPrompt = this.localStorage.get(StorageKey.UsePlayPane) === null; + this.isStickyChallengePanelAvailable = width >= 1140; + this.isStickyChallengePanelSelected = this.localStorage.get(StorageKey.UseStickyChallengePanel) === "true"; + this.showStickyChallengePanelPrompt = this.localStorage.get(StorageKey.UseStickyChallengePanel) === null; - if (!this.isMiniPlayerAvailable && this.isMiniPlayerSelected) - this.setIsStickyPanelEnabled(this.isMiniPlayerSelected, false); + if (!this.isStickyChallengePanelAvailable && this.isStickyChallengePanelSelected) + this.setIsStickyPanelEnabled(this.isStickyChallengePanelSelected, false); }), - userAppSettings.updated$.subscribe(settings => this.setIsStickyPanelEnabled(settings.useStickyChallengePanel, false)) + userAppSettings.updated$.subscribe(settings => this.setIsStickyPanelEnabled(settings.useStickyChallengePanel, false)), + this.deployState$.subscribe(state => { + this.deployStateChange.emit(state); + this.challenge = this.activeChallengesRepo.getActivePracticeChallenge(); + this.vmUrls = this.buildVmLinks(this.challenge); + }) ); } @@ -88,7 +95,7 @@ export class PlayComponent implements OnChanges { return; if (this.autoPlay && this.playerId && this.challengeSpec && this.challengeSpec.id !== changes.challengeSpec?.currentValue) { - await this.deployChallenge({ challengeSpecId: changes.challengeSpec.currentValue.id, playerId: this.playerId }); + await this.startChallenge({ challengeSpecId: changes.challengeSpec.currentValue.id, playerId: this.playerId }); } } @@ -97,37 +104,35 @@ export class PlayComponent implements OnChanges { throw new Error("Can't deploy from the Play component without a challenge."); } - this.isDeploying = true; - this.deployStatusChanged.emit(true); + this.deployState$.next("deploying"); await firstValueFrom(this.challengesService.deploy({ id: challengeId })); - this.isDeploying = false; - this.deployStatusChanged.emit(false); + this.deployState$.next("deployed"); } protected async undeployVms(challengeId: string) { - this.isUndeploying = true; + this.deployState$.next("undeploying"); await firstValueFrom(this.challengesService.undeploy({ id: challengeId })); - this.isUndeploying = false; + this.deployState$.next("undeployed"); } protected setIsStickyPanelEnabled(isEnabled: boolean, notify = true) { - this.showMiniPlayerPrompt = false; + this.showStickyChallengePanelPrompt = false; - if (this.isMiniPlayerAvailable) { - this.isMiniPlayerSelected = isEnabled; - this.localStorage.add(StorageKey.UsePlayPane, this.isMiniPlayerSelected); + if (this.isStickyChallengePanelAvailable) { + this.isStickyChallengePanelSelected = isEnabled; + this.localStorage.add(StorageKey.UseStickyChallengePanel, this.isStickyChallengePanelSelected); } else { - this.isMiniPlayerSelected = false; - this.localStorage.add(StorageKey.UsePlayPane, false); + this.isStickyChallengePanelSelected = false; + this.localStorage.add(StorageKey.UseStickyChallengePanel, false); } if (notify) { - this.userAppSettings.updated$.next({ useStickyChallengePanel: this.isMiniPlayerSelected }); + this.userAppSettings.updated$.next({ useStickyChallengePanel: this.isStickyChallengePanelSelected }); } - if (!this.isMiniPlayerSelected) { + if (!this.isStickyChallengePanelSelected) { this.windowService.scrollToBottom(); } } @@ -145,16 +150,14 @@ export class PlayComponent implements OnChanges { return vmUrls; } - private async deployChallenge(args: { challengeSpecId: string, playerId: string }) { + private async startChallenge(args: { challengeSpecId: string, playerId: string }) { if (!this.localUser.user$.value) { throw new Error("Can't deploy for an unauthed user."); } this.errors = []; this.solutionGuide = null; - - this.deployStatusChanged.emit(true); - this.isDeploying = true; + this.deployState$.next("deploying"); try { const startedChallenge = await firstValueFrom(this.challengesService.startPlaying({ @@ -163,17 +166,14 @@ export class PlayComponent implements OnChanges { userId: this.localUser.user$.value.id })); - this.challenge = this.activeChallengesRepo.getActivePracticeChallenge(); - this.vmUrls = this.buildVmLinks(this.challenge); + this.deployState$.next(startedChallenge.hasDeployedGamespace ? "deployed" : "undeployed"); // also look up the solution guide if there is one this.solutionGuide = await firstValueFrom(this.challengesService.getSolutionGuide(startedChallenge.id)); } catch (err: any) { this.errors.push(err); + this.deployState$.next("undeployed"); } - - this.isDeploying = false; - this.deployStatusChanged.emit(false); } } diff --git a/projects/gameboard-ui/src/app/standalone/users/user-picker/user-picker.component.html b/projects/gameboard-ui/src/app/standalone/users/user-picker/user-picker.component.html new file mode 100644 index 000000000..47aadec6b --- /dev/null +++ b/projects/gameboard-ui/src/app/standalone/users/user-picker/user-picker.component.html @@ -0,0 +1,19 @@ +
    + +
    + +
    +
    + + +
    + + +
    +
    diff --git a/projects/gameboard-ui/src/app/standalone/users/user-picker/user-picker.component.scss b/projects/gameboard-ui/src/app/standalone/users/user-picker/user-picker.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/projects/gameboard-ui/src/app/standalone/users/user-picker/user-picker.component.ts b/projects/gameboard-ui/src/app/standalone/users/user-picker/user-picker.component.ts new file mode 100644 index 000000000..f30e92a3e --- /dev/null +++ b/projects/gameboard-ui/src/app/standalone/users/user-picker/user-picker.component.ts @@ -0,0 +1,45 @@ +import { Component, EventEmitter, inject, Input, Output } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TypeaheadMatch, TypeaheadModule } from 'ngx-bootstrap/typeahead'; +import { debounceTime, filter, Observable, Observer, switchMap } from 'rxjs'; +import { CoreModule } from '@/core/core.module'; +import { fa } from "@/services/font-awesome.service"; +import { UserService } from '@/api/user.service'; +import { ApiUser } from '@/api/user-models'; + +@Component({ + selector: 'app-user-picker', + standalone: true, + imports: [ + CommonModule, + TypeaheadModule, + CoreModule, + ], + templateUrl: './user-picker.component.html', + styleUrls: ['./user-picker.component.scss'] +}) +export class UserPickerComponent { + @Input() eligibleForGameId?: string; + @Input() excludeUserIds: string[] = []; + @Output() select = new EventEmitter(); + + protected fa = fa; + protected searchTerm = ""; + + private userService = inject(UserService); + + protected typeaheadSearch$ = new Observable((observer: Observer) => observer.next(this.searchTerm)).pipe( + filter(s => s?.length >= 2), + debounceTime(300), + switchMap(search => this.userService.list({ + eligibleForGameId: this.eligibleForGameId, + excludeIds: this.excludeUserIds, + term: search, + })), + ); + + protected handleTypeaheadSelect(apiUserResult: TypeaheadMatch) { + this.select.emit(apiUserResult.item); + this.searchTerm = ""; + } +} diff --git a/projects/gameboard-ui/src/app/support/ticket-details/ticket-details.component.html b/projects/gameboard-ui/src/app/support/ticket-details/ticket-details.component.html index 09854c96b..9f374d0f3 100644 --- a/projects/gameboard-ui/src/app/support/ticket-details/ticket-details.component.html +++ b/projects/gameboard-ui/src/app/support/ticket-details/ticket-details.component.html @@ -1,6 +1,5 @@
    -
    + diff --git a/projects/gameboard-ui/src/app/support/ticket-details/ticket-details.component.ts b/projects/gameboard-ui/src/app/support/ticket-details/ticket-details.component.ts index c5ba932ac..943242701 100644 --- a/projects/gameboard-ui/src/app/support/ticket-details/ticket-details.component.ts +++ b/projects/gameboard-ui/src/app/support/ticket-details/ticket-details.component.ts @@ -21,6 +21,7 @@ import { LogService } from '../../services/log.service'; import { AppTitleService } from '@/services/app-title.service'; import { ConfigService } from '@/utility/config.service'; import { TicketSupportToolsContext } from '../components/ticket-support-tools/ticket-support-tools.component'; +import { UserRolePermissionsService } from '@/api/user-role-permissions.service'; @Component({ selector: 'app-ticket-details', @@ -78,6 +79,7 @@ export class TicketDetailsComponent implements AfterViewInit, OnDestroy { faExclamationCircle = faExclamationCircle; faSync = faSync; + protected sortActivityAscending = true; protected supportToolsContext?: TicketSupportToolsContext; selectedAttachmentList?: AttachmentFile[]; @@ -98,13 +100,14 @@ export class TicketDetailsComponent implements AfterViewInit, OnDestroy { private api: SupportService, private clipboard: ClipboardService, private logService: LogService, + private permissionsService: UserRolePermissionsService, private playerApi: PlayerService, private userApi: UserService, private sanitizer: DomSanitizer, private http: HttpClient, private toastsService: ToastService, ) { - const canManage$ = local.can$("Support_ManageTickets"); + const canManage$ = permissionsService.can$("Support_ManageTickets"); this.currentUser = local.user$.value; const ticket$ = combineLatest([ @@ -114,7 +117,7 @@ export class TicketDetailsComponent implements AfterViewInit, OnDestroy { map(([p, r]) => p), filter(p => !!p.id && (!this.editingContent || this.savingContent)), // don't refresh data if editing and not saving yet tap(p => this.key = p.id), - switchMap(p => api.retrieve(p.id)), + switchMap(p => api.retrieve(p.id, { sortActivityAscending: this.sortActivityAscending })), tap(t => { this.editingContent = false; this.savingContent = false; @@ -127,7 +130,6 @@ export class TicketDetailsComponent implements AfterViewInit, OnDestroy { // initialization for the "support tools" component const hasGame = t.player?.gameId && t.player?.gameName; const hasPlayer = !!t.player; - const hasTeam = t.teamId && t.teamName; this.supportToolsContext = { challenge: t.challenge ? { id: t.challengeId, name: t.challenge?.name } : undefined, @@ -501,6 +503,11 @@ export class TicketDetailsComponent implements AfterViewInit, OnDestroy { ); } + protected handleSortChange(sortAscending: boolean) { + this.sortActivityAscending = sortAscending; + this.refresh$.next(true); + } + public async copyToMarkdown(ticket: Ticket) { const ticketMarkdown = await this.api.getTicketMarkdown(ticket); await this.clipboard.copy(ticketMarkdown); diff --git a/projects/gameboard-ui/src/app/support/ticket-form/ticket-form.component.ts b/projects/gameboard-ui/src/app/support/ticket-form/ticket-form.component.ts index f0aca788b..ad67afd77 100644 --- a/projects/gameboard-ui/src/app/support/ticket-form/ticket-form.component.ts +++ b/projects/gameboard-ui/src/app/support/ticket-form/ticket-form.component.ts @@ -53,7 +53,7 @@ export class TicketFormComponent implements OnDestroy { route: ActivatedRoute, localUserService: LocalUserService ) { - this.canManage$ = localUserService.user$.pipe(map(u => this.permissionsService.can(u, "Support_ManageTickets"))); + this.canManage$ = this.permissionsService.can$("Support_ManageTickets"); this.routeSub = route.queryParams.pipe( filter(p => !!p.cid), diff --git a/projects/gameboard-ui/src/app/users/components/settings/settings.component.ts b/projects/gameboard-ui/src/app/users/components/settings/settings.component.ts index 91ba833d2..bc159466b 100644 --- a/projects/gameboard-ui/src/app/users/components/settings/settings.component.ts +++ b/projects/gameboard-ui/src/app/users/components/settings/settings.component.ts @@ -65,7 +65,6 @@ export class SettingsComponent implements OnInit { appUrl: "user/settings", // we randomize the notification tag here in order to allow most devices to repeat it without it being dismissed. // this allows users to preview the notification multiple times with multiple clicks. - allowRenotify: true, tag: `test-${Math.random()}` }); } diff --git a/projects/gameboard-ui/src/app/users/users.module.ts b/projects/gameboard-ui/src/app/users/users.module.ts index dda3589f4..f5efe7128 100644 --- a/projects/gameboard-ui/src/app/users/users.module.ts +++ b/projects/gameboard-ui/src/app/users/users.module.ts @@ -61,7 +61,7 @@ const DECLARED_COMPONENTS = [ const permissionsService = inject(UserRolePermissionsService); const localUser = inject(LocalUserService); - return permissionsService.can(localUser.user$.value, "Admin_View"); + return permissionsService.can("Admin_View"); }] } ] diff --git a/projects/gameboard-ui/src/app/utility/admin.guard.ts b/projects/gameboard-ui/src/app/utility/admin.guard.ts index e63599f25..1ef381398 100644 --- a/projects/gameboard-ui/src/app/utility/admin.guard.ts +++ b/projects/gameboard-ui/src/app/utility/admin.guard.ts @@ -34,7 +34,7 @@ export class AdminGuard implements CanActivate, CanActivateChild { childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | Promise | boolean | UrlTree { return this.localUser.user$.pipe( - map(u => this.permissionsService.can(u, "Admin_View")), + map(u => this.permissionsService.canUser(u, "Admin_View")), map(can => { if (can) return true; diff --git a/projects/gameboard-ui/src/app/utility/auth.service.ts b/projects/gameboard-ui/src/app/utility/auth.service.ts index a9e7c7238..76b285331 100644 --- a/projects/gameboard-ui/src/app/utility/auth.service.ts +++ b/projects/gameboard-ui/src/app/utility/auth.service.ts @@ -100,7 +100,9 @@ export class AuthService { logout(): void { if (this.oidcUser && this._tokenState$.getValue() === AuthTokenState.valid) { - if (this.config.environment.settings.oidc.autoLogout) { + // by default, we do a complete logout. the only condition under which we log out + // of the app but not the IDP is this setting is specified and is set to false + if (this.config.environment.settings.oidc.autoLogout !== false) { this.mgr.signoutRedirect() .then(() => { }) .catch(err => { diff --git a/projects/gameboard-ui/src/app/utility/config.service.ts b/projects/gameboard-ui/src/app/utility/config.service.ts index 93e3394c6..13af93b5a 100644 --- a/projects/gameboard-ui/src/app/utility/config.service.ts +++ b/projects/gameboard-ui/src/app/utility/config.service.ts @@ -208,8 +208,8 @@ export interface LocalAppSettings { export interface AppUserManagerSettings extends UserManagerSettings { authority: string; - autoLogin: boolean; - autoLogout: boolean; + autoLogin?: boolean; + autoLogout?: boolean; client_id: string; debug?: boolean; redirect_uri: string; diff --git a/projects/gameboard-ui/src/app/utility/services/toast.service.ts b/projects/gameboard-ui/src/app/utility/services/toast.service.ts index 8afa4d7a0..56c3211a0 100644 --- a/projects/gameboard-ui/src/app/utility/services/toast.service.ts +++ b/projects/gameboard-ui/src/app/utility/services/toast.service.ts @@ -14,7 +14,7 @@ export interface ToastOptions { // called when the toast is dismissed callback?: () => void; // called when the user clicks the toast - onClick?: () => void; + onClick?: () => Promise; showCloseIcon?: boolean; } @@ -65,6 +65,11 @@ export class ToastService { return { ...options, ...this.FIXED_OPTIONS, + onClick: () => { + if (options.onClick) { + options.onClick(); + } + }, close: options.showCloseIcon, text: textTemplate }; diff --git a/projects/gameboard-ui/src/app/utility/user.service.ts b/projects/gameboard-ui/src/app/utility/user.service.ts index 6120e1c4b..aa08be710 100644 --- a/projects/gameboard-ui/src/app/utility/user.service.ts +++ b/projects/gameboard-ui/src/app/utility/user.service.ts @@ -10,8 +10,6 @@ import { UserService as ApiUserService } from '../api/user.service'; import { AuthService, AuthTokenState } from './auth.service'; import { ConfigService } from './config.service'; import { LogService } from '@/services/log.service'; -import { UserRolePermissionsService } from '@/api/user-role-permissions.service'; -import { UserRolePermissionKey } from '@/api/user-role-permissions.models'; @Injectable({ providedIn: 'root' }) export class UserService implements OnDestroy { @@ -23,12 +21,10 @@ export class UserService implements OnDestroy { constructor( private auth: AuthService, private log: LogService, - private permissionsService: UserRolePermissionsService, api: ApiUserService, config: ConfigService, router: Router ) { - // when token updated, or every half hour grab a fresh mks cookie if token still good combineLatest([ timer(1000, 1800000), @@ -39,7 +35,10 @@ export class UserService implements OnDestroy { filter(t => t === AuthTokenState.valid && !!auth.oidcUser?.profile), map(tokenState => auth.oidcUser?.profile as unknown as UserOidcProfile), switchMap(profile => api.tryCreate({ id: profile.sub })), - catchError(err => of(null)), + catchError(err => { + this.log.logError("Auth error", err); + return of(null); + }), filter(result => !!result), ).subscribe(result => this.user$.next(result!.user)); @@ -58,21 +57,12 @@ export class UserService implements OnDestroy { // log the login event for the current user (we track date of last login and total login count) this._userSub = this.auth.tokenState$.pipe( + map(t => t === AuthTokenState.valid), distinctUntilChanged(), - filter(t => t === AuthTokenState.valid && !!auth.oidcUser?.profile) + filter(t => t && !!auth.oidcUser?.profile) ).subscribe(async u => await firstValueFrom(api.updateLoginEvents())); } - can(permission: UserRolePermissionKey) { - return this.permissionsService.can(this.user$.value, permission); - } - - can$(permission: UserRolePermissionKey) { - return this.user$.pipe( - map(u => this.permissionsService.can(u, permission)) - ); - } - ngOnDestroy(): void { this._userSub?.unsubscribe(); } diff --git a/projects/gameboard-ui/src/environments/environment.ts b/projects/gameboard-ui/src/environments/environment.ts index 5b9cf60f0..f003bda2c 100644 --- a/projects/gameboard-ui/src/environments/environment.ts +++ b/projects/gameboard-ui/src/environments/environment.ts @@ -16,8 +16,6 @@ export const environment: Environment = { isProduction: false, oidc: { authority: 'http://localhost:8080/realms/foundry', - autoLogin: false, - autoLogout: true, client_id: 'dev.gameboard.web', redirect_uri: 'http://localhost:4202/oidc', silent_redirect_uri: 'http://localhost:4202/assets/oidc-silent.html', diff --git a/projects/gameboard-ui/src/index.html b/projects/gameboard-ui/src/index.html index 3ea4d7d10..6263ef255 100644 --- a/projects/gameboard-ui/src/index.html +++ b/projects/gameboard-ui/src/index.html @@ -6,10 +6,7 @@ Gameboard - - - - + diff --git a/projects/gameboard-ui/src/scss/_toastify.scss b/projects/gameboard-ui/src/scss/_toastify.scss index 31057e7f7..72bebd8a8 100644 --- a/projects/gameboard-ui/src/scss/_toastify.scss +++ b/projects/gameboard-ui/src/scss/_toastify.scss @@ -20,6 +20,10 @@ strong { color: $success; } + + em { + color: $muted; + } } svg { diff --git a/projects/gameboard-ui/src/scss/_variables.scss b/projects/gameboard-ui/src/scss/_variables.scss index e21f2324e..c0f45015d 100644 --- a/projects/gameboard-ui/src/scss/_variables.scss +++ b/projects/gameboard-ui/src/scss/_variables.scss @@ -40,6 +40,7 @@ $danger: $red !default; $light: $gray-600 !default; $dark: $gray-800 !default; $foreground: $gray-100 !default; +$muted: $light !default; $yiq-contrasted-threshold: 175 !default; diff --git a/projects/gameboard-ui/src/styles.scss b/projects/gameboard-ui/src/styles.scss index 788a0e23a..3e708e7df 100644 --- a/projects/gameboard-ui/src/styles.scss +++ b/projects/gameboard-ui/src/styles.scss @@ -485,11 +485,11 @@ th[align="left"] { } .h-auto { - height: auto; + height: auto !important; } .h-100 { - height: 100%; + height: 100% !important; } .width-5 {