Skip to content

Commit

Permalink
v3.28.2 (#223)
Browse files Browse the repository at this point in the history
* WIP timeline updates

* Add ticket stuff to timeline

* Update tooltip markdown for timestamp

* Fix ticket label picker modal search

* Ticket details styling

* Minor styling to ticket details, fix certificate stuff

* Fix support pill
  • Loading branch information
sei-bstein authored Jan 22, 2025
1 parent 85be2ed commit 8120413
Show file tree
Hide file tree
Showing 17 changed files with 171 additions and 78 deletions.
1 change: 1 addition & 0 deletions projects/gameboard-ui/src/app/api/certificates.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export class CertificatesService {
}

getCompetitiveCertificates(userId: string): Promise<CompetitiveModeCertificate[]> {
console.log("getting certs", userId);
return firstValueFrom(this.http.get<CompetitiveModeCertificate[]>(this.apiUrl.build(`user/${userId}/certificates/competitive`)));
}

Expand Down
16 changes: 14 additions & 2 deletions projects/gameboard-ui/src/app/api/event-horizon.models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ export type EventHorizonEventType = "challengeStarted" |
"gamespaceOnOff" |
"solveComplete" |
"submissionRejected" |
"submissionScored"
"submissionScored" |
"ticketOpenClose"

export interface EventHorizonGenericEvent {
id: string;
Expand Down Expand Up @@ -35,7 +36,18 @@ export interface EventHorizonSolveCompleteEvent extends EventHorizonGenericEvent
};
}

export type EventHorizonEvent = EventHorizonGenericEvent | EventHorizonGamespaceOnOffEvent | EventHorizonSubmissionScoredEvent | EventHorizonSolveCompleteEvent;
export interface EventHorizonTicketOpenCloseEvent extends EventHorizonGenericEvent {
eventData: {
closedAt?: DateTime;
ticketKey: string;
}
}

export type EventHorizonEvent = EventHorizonGenericEvent |
EventHorizonGamespaceOnOffEvent |
EventHorizonSubmissionScoredEvent |
EventHorizonSolveCompleteEvent |
EventHorizonTicketOpenCloseEvent;

export interface EventHorizonChallenge {
id: string;
Expand Down
12 changes: 10 additions & 2 deletions projects/gameboard-ui/src/app/api/event-horizon.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom, map, of } from 'rxjs';
import { EventHorizonEventType, EventHorizonEvent, TeamEventHorizonViewModel, EventHorizonChallengeSpec, EventHorizonGamespaceOnOffEvent } from './event-horizon.models';
import { EventHorizonEventType, EventHorizonEvent, TeamEventHorizonViewModel, EventHorizonChallengeSpec, EventHorizonGamespaceOnOffEvent, EventHorizonTicketOpenCloseEvent } from './event-horizon.models';
import { ApiUrlService } from '@/services/api-url.service';
import { ApiDateTimeService } from '@/services/api-date-time.service';
import { LogService } from '@/services/log.service';
Expand Down Expand Up @@ -44,6 +44,13 @@ export class EventHorizonService {
const offAtStamp = this.apiDateTimeService.toDateTime(asGamespaceEvent.eventData?.offAt as any);
asGamespaceEvent.eventData.offAt = offAtStamp || undefined;
}

// also true of tickety events
const asTicketEvent = event as EventHorizonTicketOpenCloseEvent;
if (asTicketEvent.eventData?.closedAt) {
const closedAtStamp = this.apiDateTimeService.toDateTime(asTicketEvent.eventData.closedAt as any);
asTicketEvent.eventData.closedAt = closedAtStamp || undefined;
}
}

return timeline;
Expand All @@ -67,7 +74,8 @@ export class EventHorizonService {
"gamespaceOnOff",
"solveComplete",
"submissionRejected",
"submissionScored"
"submissionScored",
"ticketOpenClose"
];
}

Expand Down
10 changes: 7 additions & 3 deletions projects/gameboard-ui/src/app/components/nav/nav.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@
<a *ngFor="let t of toc$ | async" class="btn btn-link text-success mx-1" routerLinkActive="active"
[routerLink]="['doc', t.link]">{{t.display}}</a>
</ng-container>

<a *ngIf="isPracticeModeEnabled" class="btn btn-link text-success mx-1" routerLinkActive="active"
[routerLink]="['practice']">Practice</a>

<ng-container *ngIf="user$ | async as user; else unauthed">
<a class="btn btn-link text-success mx-1" routerLinkActive="active" [routerLink]="['support']">
<span>Support</span>
<div>
<a class="btn btn-link text-success mx-1" [routerLink]="['support']" routerLinkActive="active">
Support
</a>
<app-support-pill></app-support-pill>
</a>
</div>

<a class="btn btn-link text-success mx-1" routerLinkActive="active" [routerLink]="profileUrl">Profile</a>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<app-modal-content title="Challenge State" subtitle="">

</app-modal-content>
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CoreModule } from "../../core.module";

@Component({
selector: 'app-challenge-yaml-modal',
standalone: true,
imports: [CommonModule, CoreModule],
templateUrl: './challenge-yaml-modal.component.html',
styleUrls: ['./challenge-yaml-modal.component.scss']
})
export class ChallengeYamlModalComponent implements OnInit {
challengeId?: string;

ngOnInit(): void {
if (!this.challengeId) {
throw new Error("challengeId is required.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,27 @@
}

.eh-event-type-challenge-started {
background-color: #e7eaf6;
background-color: #9cdaa9;
}

.eh-event-type-gamespace {
background-color: #a2a8d3;
.eh-event-type-gamespace-on-off {
background-color: #ceecd4;
font-weight: bold;
color: #eee;
}

.eh-event-type-solve-complete {
background-color: #38598b;
background-color: #41ad57;
}

.eh-event-type-submission-scored {
background-color: #113f67;
color: #e7eaf6;
background-color: #318241;
color: #eee;
}

.eh-event-type-ticket-open-close {
background-color: $warning;
color: #fff;
}

.vis-label {
Expand All @@ -29,6 +36,7 @@
font-size: 1rem !important;
font-weight: bold;
margin: 0;
padding: 0 1rem;
}

h2 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { EventHorizonDataItem, EventHorizonEventType, TeamEventHorizonViewModel
import { EventHorizonService } from '@/api/event-horizon.service';
import { EventHorizonRenderingService } from '@/services/event-horizon-rendering.service';
import { LogService } from '@/services/log.service';
import { ModalConfirmService } from '@/services/modal-confirm.service';
import { ClipboardService } from '@/utility/services/clipboard.service';
import { ToastService } from '@/utility/services/toast.service';
import { AfterViewInit, Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
Expand Down Expand Up @@ -60,12 +59,9 @@ export class TeamEventHorizonComponent implements OnInit, AfterViewInit, OnDestr
const teamChallengeInstance = this.timelineViewModel?.team.challenges.find(i => i.specId == c.id);
return {
id: c.id,
options: {

},
title: c.name,
content: `
<h1>${c.name}</h1>
<h1 class="clonk-on-me">${c.name}</h1>
<h2>
${teamChallengeInstance ? `${teamChallengeInstance.id.substring(0, 6)} &middot; ${teamChallengeInstance.score}/${c.maxPossibleScore} points` : "unlaunched"}
</h2>
Expand Down Expand Up @@ -95,13 +91,14 @@ export class TeamEventHorizonComponent implements OnInit, AfterViewInit, OnDestr

const timelineEvent = this.eventHorizonService.getEventId(eventId, this.timelineViewModel);
const spec = this.eventHorizonService.getSpecForEventId(this.timelineViewModel, timelineEvent.id);
const bodyContent = this.eventHorizonRenderingService.toModalHtmlContent(timelineEvent, spec);
const bodyContent = this.eventHorizonRenderingService.toModalMarkdown(timelineEvent, spec);

if (!bodyContent)
if (!bodyContent) {
return;
}

await this.clipboardService.copy(bodyContent);
this.toastService.showMessage(`Copied this ** ${this.eventHorizonRenderingService.toFriendlyName(timelineEvent.type)}** event to your clipboard.`);
this.toastService.showMessage(`Copied this **${this.eventHorizonRenderingService.toFriendlyName(timelineEvent.type)}** event to your clipboard.`);
}

protected async handleEventTypeToggled(eventType: EventHorizonEventType) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { EventHorizonDataItem, EventHorizonGenericEvent, EventHorizonEventType, EventHorizonSolveCompleteEvent, EventHorizonSubmissionScoredEvent, EventHorizonChallengeSpec, EventHorizonViewOptions, TeamEventHorizonViewModel, EventHorizonGamespaceOnOffEvent } from '@/api/event-horizon.models';
import { EventHorizonDataItem, EventHorizonGenericEvent, EventHorizonEventType, EventHorizonSolveCompleteEvent, EventHorizonSubmissionScoredEvent, EventHorizonChallengeSpec, EventHorizonViewOptions, TeamEventHorizonViewModel, EventHorizonGamespaceOnOffEvent, EventHorizonTicketOpenCloseEvent } from '@/api/event-horizon.models';
import { Injectable } from '@angular/core';
import { DateTime } from 'luxon';
import { MarkdownHelpersService } from './markdown-helpers.service';
Expand All @@ -21,7 +21,7 @@ export class EventHorizonRenderingService {
groupTemplate: (groupData: any, element: any) => this.toGroupTemplate(groupData),
min: eventHorizonVm.team.session.start.toJSDate(),
max: sessionEnd.toJSDate(),
selectable: true,
orientation: "top",
stack: true,
start: eventHorizonVm.team.session.start.toJSDate(),
tooltip: {
Expand All @@ -40,9 +40,10 @@ export class EventHorizonRenderingService {
return this.toGamespaceOnOffDataItem(timelineEvent, challengeSpec);
case "solveComplete":
return this.toSolveCompleteDataItem(timelineEvent, challengeSpec);
case "submissionScored": {
case "submissionScored":
return this.toSubmissionScoredDataItem(timelineEvent, challengeSpec);
}
case "ticketOpenClose":
return this.toTicketOpenCloseDataItem(timelineEvent as EventHorizonTicketOpenCloseEvent, challengeSpec);
}
throw new Error("Timeline event type not templated.");
}
Expand All @@ -59,8 +60,10 @@ export class EventHorizonRenderingService {
return "Submission Rejected (Session Expired)";
case "submissionScored":
return "Submission";
case "ticketOpenClose":
return "Active Ticket";
default:
throw new Error(`Couldn't find a friendly name for event type "${eventType}".`);
return eventType;
}
}

Expand All @@ -71,7 +74,7 @@ export class EventHorizonRenderingService {
return `<div class="eh-group">${groupData.content}</div>`;
}

public toModalHtmlContent(timelineEvent: EventHorizonGenericEvent, challengeSpec: EventHorizonChallengeSpec, includeClipboardPrompt?: boolean): string {
public toModalMarkdown(timelineEvent: EventHorizonGenericEvent, challengeSpec: EventHorizonChallengeSpec, includeClipboardPrompt?: boolean): string {
if (!timelineEvent)
return "";

Expand All @@ -88,6 +91,8 @@ export class EventHorizonRenderingService {
case "submissionScored":
detail = this.toSubmissionScoredMarkdown(timelineEvent as EventHorizonSubmissionScoredEvent, challengeSpec);
break;
case "ticketOpenClose":
detail = this.toTicketOpenCloseMarkdown(timelineEvent as EventHorizonTicketOpenCloseEvent);
}

let retVal = header;
Expand Down Expand Up @@ -115,8 +120,7 @@ export class EventHorizonRenderingService {
if (challengeSpec.maxAttempts)
attemptSummary = `${attemptSummary}/${challengeSpec.maxAttempts}`;

return `
**Attempt:** ${attemptSummary}
return `**Attempt:** ${attemptSummary}
**Points after this attempt:** ${timelineEvent.eventData.score}/${challengeSpec.maxPossibleScore}
Expand All @@ -135,6 +139,13 @@ export class EventHorizonRenderingService {
`.trim();
}

private toTicketOpenCloseMarkdown(timelineEvent: EventHorizonTicketOpenCloseEvent) {
let firstTicketText = `The team opened ticket **${timelineEvent.eventData.ticketKey}** here.`;
let closedInfo = timelineEvent.eventData.closedAt ? ` We fully closed it at ${timelineEvent.eventData.closedAt.toLocaleString(DateTime.DATETIME_MED)}.` : "";

return `${firstTicketText}${closedInfo}`;
}

private toGenericDataItem(timelineEvent: EventHorizonGenericEvent, challengeSpec: EventHorizonChallengeSpec, eventName: string, className: string, isClickable = false): EventHorizonDataItem {
return {
id: timelineEvent.id,
Expand All @@ -143,13 +154,13 @@ export class EventHorizonRenderingService {
content: eventName,
className: `eh-event ${isClickable ? "eh-event-clickable" : ""} ${className}`,
isClickable,
title: this.markdownHelpers.toHtml(this.toModalHtmlContent(timelineEvent, challengeSpec, true)),
title: this.markdownHelpers.toHtml(this.toModalMarkdown(timelineEvent, challengeSpec, true)),
eventData: null
};
}

private toGamespaceOnOffDataItem(timelineEvent: EventHorizonGenericEvent, challengeSpec: EventHorizonChallengeSpec): EventHorizonDataItem {
const typedEvent = timelineEvent as unknown as EventHorizonGamespaceOnOffEvent;
const typedEvent = timelineEvent as EventHorizonGamespaceOnOffEvent;
const baseItem = this.toGenericDataItem(timelineEvent, challengeSpec, "Gamespace On", "eh-event-type-gamespace-on-off", false);

baseItem.end = typedEvent.eventData?.offAt ? typedEvent.eventData.offAt.toJSDate() : this.nowService.now();
Expand All @@ -160,18 +171,29 @@ export class EventHorizonRenderingService {
}

private toSolveCompleteDataItem(timelineEvent: EventHorizonGenericEvent, challengeSpec: EventHorizonChallengeSpec): EventHorizonDataItem {
const typedEvent = timelineEvent as unknown as EventHorizonSolveCompleteEvent;
const typedEvent = timelineEvent as EventHorizonSolveCompleteEvent;
const baseItem = this.toGenericDataItem(timelineEvent, challengeSpec, "Completed", "eh-event-type-challenge-complete", true);
baseItem.eventData = typedEvent;

return baseItem;
}

private toSubmissionScoredDataItem(timelineEvent: EventHorizonGenericEvent, challengeSpec: EventHorizonChallengeSpec): EventHorizonDataItem {
const typedEvent = timelineEvent as unknown as EventHorizonSubmissionScoredEvent;
const typedEvent = timelineEvent as EventHorizonSubmissionScoredEvent;
const baseItem = this.toGenericDataItem(timelineEvent, challengeSpec, "Submission", "eh-event-type-submission-scored", true);
baseItem.eventData = typedEvent;

return baseItem;
}

private toTicketOpenCloseDataItem(timelineEvent: EventHorizonTicketOpenCloseEvent, challengeSpec: EventHorizonChallengeSpec): EventHorizonDataItem {
const typedEvent = timelineEvent as EventHorizonTicketOpenCloseEvent;
const baseItem = this.toGenericDataItem(timelineEvent, challengeSpec, `Ticket ${timelineEvent.eventData.ticketKey}`, "eh-event-type-ticket-open-close", true);

baseItem.end = typedEvent.eventData?.closedAt?.toJSDate() || this.nowService.now();
baseItem.eventData = typedEvent;
baseItem.type = "range";

return baseItem;
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Injectable } from '@angular/core';
import { inject, Injectable, SecurityContext } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { MarkdownService } from 'ngx-markdown';

@Injectable({ providedIn: 'root' })
export class MarkdownHelpersService {
constructor(private markdownService: MarkdownService) { }
private domSanitizer = inject(DomSanitizer);
private mdService = inject(MarkdownService);

arrayToBulletList(items: string[]): string {
return items.map(i => `\n - ${i}`).join('');
Expand All @@ -19,15 +21,15 @@ export class MarkdownHelpersService {
getMarkdownPlaceholderHelp(header?: string): string {
const paragraphs: string[] = [];

if (header)
if (header) {
paragraphs.push(header);

}
paragraphs.push("This is a markdown field. You can surround text with _underscores_ to make it italic or double **asterisks** to make it bold. You can also [link to content using this syntax](https://google.com).");

return paragraphs.join("\n\n").trim();
}

toHtml(markdownContent: string) {
return this.markdownService.parse(markdownContent);
toHtml(markdownContent: string): string {
return this.domSanitizer.sanitize(SecurityContext.HTML, this.domSanitizer.bypassSecurityTrustHtml(this.mdService.parse(markdownContent, { disableSanitizer: true }))) || "";
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
<app-modal-content title="Filter by labels" [subtitle]="selectedCount + ' selected'" confirmButtonText="Filter"
<app-modal-content title="Filter by labels" [subtitle]="selectedCount + ' labels selected'" confirmButtonText="Filter"
(confirm)="handleConfirmSelections()">
<div *ngIf="selectedCount < totalLabelCount; else allLabelsSelected">
<div class="mb-4">
<input type="text" class="form-control" placeholder="Search tags to filter by..." [(ngModel)]="searchText">
</div>
<ul class="d-flex flex-wrap align-items-center mb-0">
<li *ngFor="let entry of labels | keyvalue | filter:'value':false" class="mr-2 mb-2">
<app-colored-text-chip [text]="entry.key" [isClickable]="true" (click)="handleLabelSelection(entry.key)"
colorMode="dim"></app-colored-text-chip>
</li>
<ng-container *ngFor="let entry of labels | keyvalue | filter:'value':false">
<li *ngIf="!searchText || entry.key.indexOf(searchText) >= 0">
<app-colored-text-chip [text]="entry.key" [isClickable]="true"
(click)="handleLabelSelection(entry.key)" colorMode="dim"></app-colored-text-chip>
</li>
</ng-container>
</ul>
</div>
<hr />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +0,0 @@
li {
// flex-basis: 30%;
}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<fa-icon *ngIf="count > 0" [icon]="faPill" class="text-warning"></fa-icon>
<fa-icon *ngIf="count > 0" [icon]="faPill" class="ml-2 text-warning"></fa-icon>
Loading

0 comments on commit 8120413

Please sign in to comment.