Skip to content

Commit

Permalink
v3.26.0 (#210)
Browse files Browse the repository at this point in the history
* MVP of feedback template stuff

* Theming
  • Loading branch information
sei-bstein authored Dec 12, 2024
1 parent be822f0 commit 6eb4c0e
Show file tree
Hide file tree
Showing 48 changed files with 962 additions and 550 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ <h5>Challenges</h5>
<td class="text-center">
<span
[class.not-deployed]="!(spec.id | externalSpecIdToChallenge:team.challenges)!.challengeCreated"
[class.text-info]="(spec.id | externalSpecIdToChallenge:team.challenges)!.challengeCreated">
[class.text-success]="(spec.id | externalSpecIdToChallenge:team.challenges)!.challengeCreated">
{{((spec.id |
externalSpecIdToChallenge:team.challenges)!.challengeCreated ? "Yes" :
"No")}}
Expand All @@ -93,7 +93,7 @@ <h5>Challenges</h5>
<td class="text-center">
<span
[class.not-deployed]="!(spec.id | externalSpecIdToChallenge:team.challenges)!.gamespaceDeployed"
[class.text-info]="(spec.id | externalSpecIdToChallenge:team.challenges)!.gamespaceDeployed">
[class.text-success]="(spec.id | externalSpecIdToChallenge:team.challenges)!.gamespaceDeployed">
{{((spec.id |
externalSpecIdToChallenge:team.challenges)!.gamespaceDeployed
? "Yes" : "No")}}
Expand All @@ -115,7 +115,7 @@ <h5>Challenges</h5>
<div class="card-section" *ngIf="team.externalGameHostUrl">
<h5>Game Host URL</h5>

<div class="cursor-pointer text-info"
<div class="cursor-pointer text-success"
tooltip="Copy this team's external host URL to your clipboard"
[appCopyOnClick]="team.externalGameHostUrl"
appCopyOnClickMessage="Copied this team's external host URL to your clipboard.">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,37 +1,28 @@
<div class="form-group pb-0 pt-1">
<label for="feedbackConfig-input">Feedback Questions</label>
<label class="d-block" for="feedbackConfig-input">Feedback Questions</label>
<div class="my-2 p-1 badge badge-warning">DEPRECATED</div>
<textarea rows="11" type="text" class="form-control font-fixed-width" id="feedbackConfig-input"
name="feedbackConfig" [(ngModel)]="boundYaml" (input)="updateYaml(boundYaml)"
[placeholder]="sampleConfig"></textarea>

<alert *ngIf="validationMessages.length" type="warning" class="mt-3">
<ul class="m-0">
<li *ngFor="let message of validationMessages">
<markdown [data]="message"></markdown>
</li>
</ul>
</alert>
name="feedbackConfig" [(ngModel)]="boundYaml" (input)="updateYaml(boundYaml)" [placeholder]="sampleConfig"
[readOnly]="true" [disabled]="true"></textarea>

<div class="d-flex align-items-center mt-3">
<div *ngIf="feedbackTemplate" class="flex-grow-1">
<ng-container *ngIf="feedbackTemplate.game?.length || feedbackTemplate.challenge?.length">
<ng-container *ngIf="feedbackTemplate.game?.length">
<strong class="text-info">{{feedbackTemplate.game.length}}</strong>
<strong class="text-success">{{feedbackTemplate.game.length}}</strong>
game questions
</ng-container>
<span *ngIf="feedbackTemplate?.game?.length && feedbackTemplate?.challenge?.length"> // </span>
<ng-container *ngIf="feedbackTemplate.challenge?.length">
<strong class="text-info">{{feedbackTemplate.challenge.length}}</strong>
<strong class="text-success">{{feedbackTemplate.challenge.length}}</strong>
challenge questions
</ng-container>
</ng-container>
</div>
<div class="btn-link mr-2 text-right flex-grow-1" (click)="handleAboutFeedbackClick()">
About feedback templates
</div>
<button type="button" class="btn btn-success ml-2 flex-basis-50" [disabled]="!sampleConfig"
(click)="handlePasteSample()">
Paste Example Configuration

<button *ngIf="feedbackTemplate" type="button" class="btn btn-success ml-2 flex-basis-50"
[disabled]="!sampleConfig" [appCopyOnClick]="feedbackTemplate.content">
Copy Deprecated Template to Clipboard
</button>
</div>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
</label>
<label>Visible</label>
</div>
<small></small>
</div>

<div class="form-group pb-0 pt-1">
Expand Down Expand Up @@ -148,9 +147,16 @@
</div>

<div class="col-12 mt-5">
<!-- <app-feedback-template-picker></app-feedback-template-picker> -->
<app-feedback-editor [feedbackTemplate]="game.feedbackTemplate"
(templateChange)="handleFeedbackTemplateChange($event || undefined)"></app-feedback-editor>
<app-feedback-template-picker labelText="Game Feedback Template"
[(templateId)]="game.feedbackTemplateId"
(select)="handleGameFeedbackTemplateChanged($event)"></app-feedback-template-picker>

<app-feedback-template-picker labelText="Challenges Feedback Template"
[(templateId)]="game.challengesFeedbackTemplateId"
(select)="handleChallengesFeedbackTemplateChanged($event)"></app-feedback-template-picker>

<app-feedback-editor *ngIf="game.feedbackConfig" [feedbackTemplate]="game.feedbackTemplate"
(templateChange)="handleFeedbackTemplateChangeOld($event || undefined)"></app-feedback-editor>
</div>

<div class="col-12 mt-5">
Expand Down Expand Up @@ -276,7 +282,6 @@
</div>

<div *ngIf="selectedSubTab === 'registration'" class="row mb-4">

<!-- right (image) -->
<div class="col-lg-6 col-sm-12">
<h4>Execution</h4>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { UnsubscriberService } from '@/services/unsubscriber.service';
import { YamlService } from '@/services/yaml.service';
import { ToastService } from '@/utility/services/toast.service';
import { ActivatedRoute } from '@angular/router';
import { FeedbackTemplateView } from '@/feedback/feedback.models';

export type SelectedSubTab = "settings" | "modes" | "registration";

Expand Down Expand Up @@ -88,7 +89,25 @@ export class GameCenterSettingsComponent implements AfterViewInit {
}
}

protected async handleFeedbackTemplateChange(template?: FeedbackTemplate) {
protected async handleChallengesFeedbackTemplateChanged(template?: FeedbackTemplateView) {
if (!this.game) {
throw new Error("Game is required");
}

this.game.challengesFeedbackTemplateId = template?.id;
await firstValueFrom(this.gameService.update(this.game));
}

protected async handleGameFeedbackTemplateChanged(template?: FeedbackTemplateView) {
if (!this.game) {
throw new Error("Game is required");
}

this.game.feedbackTemplateId = template?.id;
await firstValueFrom(this.gameService.update(this.game));
}

protected async handleFeedbackTemplateChangeOld(template?: FeedbackTemplate) {
if (!this.game)
throw new Error("Game is required");

Expand All @@ -105,8 +124,9 @@ export class GameCenterSettingsComponent implements AfterViewInit {
}

protected async handleModeChange(event: Event) {
if (!this.game)
if (!this.game) {
throw new Error("Game is required.");
}

const gameMode = ((event?.target as any).value as GameEngineMode);
this.game.mode = gameMode;
Expand Down
2 changes: 2 additions & 0 deletions projects/gameboard-ui/src/app/api/board-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ export interface BoardGame {
division: string;
mode: GameEngineMode;
sponsor: string;
challengesFeedbackTemplateId?: string;
feedbackTemplateId?: string;
feedbackTemplate: FeedbackTemplate;
background: string;
logo: string;
Expand Down
2 changes: 1 addition & 1 deletion projects/gameboard-ui/src/app/api/feedback-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export interface FeedbackReportDetails {
challengeTag?: string;
}

export interface FeedbackSubmission {
export interface FeedbackSubmissionOldAndGross {
challengeId?: string;
challengeSpecId?: string;
gameId: string;
Expand Down
55 changes: 37 additions & 18 deletions projects/gameboard-ui/src/app/api/feedback.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,51 +3,62 @@

import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { firstValueFrom, Observable, of } from 'rxjs';
import { firstValueFrom, Observable, of, Subject } from 'rxjs';
import { ConfigService } from '../utility/config.service';
import { Feedback, FeedbackQuestion, FeedbackReportDetails, FeedbackSubmission, FeedbackTemplate, QuestionType } from './feedback-models';
import { Feedback, FeedbackQuestion, FeedbackReportDetails, FeedbackSubmissionOldAndGross, FeedbackTemplate, QuestionType } from './feedback-models';
import { YamlService } from '@/services/yaml.service';
import { hasProperty } from '@/../tools/functions';
import { unique } from '@/../tools/tools';
import { CreateFeedbackTemplate, FeedbackTemplateView, ListFeedbackTemplatesResponse } from '@/feedback/feedback.models';
import { AbstractControl, FormControl, ValidationErrors, ValidatorFn } from '@angular/forms';
import { CreateFeedbackTemplate, FeedbackQuestionsConfig, FeedbackSubmissionUpsert, FeedbackSubmissionView, FeedbackTemplateView, GetFeedbackSubmissionRequest, ListFeedbackTemplatesResponse } from '@/feedback/feedback.models';
import { AbstractControl, ValidatorFn } from '@angular/forms';
import { ApiUrlService } from '@/services/api-url.service';

@Injectable({ providedIn: 'root' })
export class FeedbackService {
private url = '';
private yamlService = inject(YamlService);

private _deleted$ = new Subject<string>();
public deleted$ = this._deleted$.asObservable();

constructor(
config: ConfigService,
private apiUrl: ApiUrlService,
private http: HttpClient
) {
this.url = config.apphost + 'api';
}

public buildQuestionsFromTemplateContent(content: string): FeedbackQuestionsConfig {
return this.yamlService.parse<FeedbackQuestionsConfig>(content);
}

public async createTemplate(template: CreateFeedbackTemplate) {
return await firstValueFrom(this.http.post<FeedbackTemplateView>(`${this.url}/feedback/template`, template));
}

public async deleteTemplate(template: FeedbackTemplateView) {
return await firstValueFrom(this.http.delete(`${this.url}/feedback/template/${template.id}`));
await firstValueFrom(this.http.delete(`${this.url}/feedback/template/${template.id}`));
this._deleted$.next(template.id);
}

public async getTemplates(): Promise<ListFeedbackTemplatesResponse> {
return await firstValueFrom(this.http.get<ListFeedbackTemplatesResponse>(`${this.url}/feedback/template`));
public async getTemplate(templateId: string): Promise<FeedbackTemplateView> {
return await firstValueFrom(this.http.get<FeedbackTemplateView>(`${this.url}/feedback/template/${templateId}`));
}

public getRequiredProperties(): string[] {
return ["id", "prompt", "type"];
public async getTemplates(): Promise<FeedbackTemplateView[]> {
const response = await firstValueFrom(this.http.get<ListFeedbackTemplatesResponse>(`${this.url}/feedback/template`));
return response.templates;
}

public getTemplateQuestionsValidator(): ValidatorFn {
return (formControl: AbstractControl<any, any>) => {
const set = this.yamlService.parse<FeedbackQuestion[]>(formControl.value);
const set = this.buildQuestionsFromTemplateContent(formControl.value);

if (!formControl.value)
return null;

if (!Array.isArray(set)) {
if (!Array.isArray(set.questions)) {
return {
questionValidation: {
valid: false,
Expand All @@ -56,7 +67,7 @@ export class FeedbackService {
};
}

if (!set?.length) {
if (!set?.questions?.length) {
return {
questionValidation: {
valid: false,
Expand All @@ -67,22 +78,22 @@ export class FeedbackService {

const retVal: string[] = [];
const requiredProperties: (keyof FeedbackQuestion)[] = ["id", "prompt", "type"];
const uniqueIds = unique(set.map(q => q?.id));
const uniqueIds = unique(set.questions.map(q => q?.id));

if (uniqueIds.length !== set.length) {
if (uniqueIds.length !== set.questions.length) {
retVal.push(`Ensure the **id** property of all questions are unique.`);
}

for (let i = 0; i < set.length; i++) {
for (let i = 0; i < set.questions.length; i++) {
const displayIndex = i + 1;
// the user-friendly 1-indexed question
// ensure the question has a legal id
if (!set[i]?.id) {
if (!set.questions[i]?.id) {
retVal.push(`Question ${displayIndex} can't have a blank or missing ID.`);
}

for (const p of requiredProperties) {
if (!hasProperty(set[i], p))
if (!hasProperty(set.questions[i], p))
retVal.push(`Question ${displayIndex} is missing required property **${p}**.`);
}
}
Expand All @@ -104,6 +115,10 @@ export class FeedbackService {
return await this.yamlService.loadSample("feedback-config");
}

public async getSubmission(request: GetFeedbackSubmissionRequest): Promise<FeedbackSubmissionView | null> {
return await firstValueFrom(this.http.get<FeedbackSubmissionView | null>(this.apiUrl.build("feedback/submission", request)));
}

public async getTemplateSampleYaml(): Promise<string | null> {
return await this.yamlService.loadSample("feedback-template-content");
}
Expand All @@ -116,7 +131,11 @@ export class FeedbackService {
return this.http.get<Feedback>(`${this.url}/feedback`, { params: search });
}

public submit(model: FeedbackSubmission): Observable<Feedback> {
public save(submission: FeedbackSubmissionUpsert): Promise<FeedbackSubmissionView> {
return firstValueFrom(this.http.post<FeedbackSubmissionView>(`${this.url}/feedback`, submission));
}

public submit(model: FeedbackSubmissionOldAndGross): Observable<Feedback> {
return this.http.put<Feedback>(`${this.url}/feedback/submit`, model);
}

Expand Down
2 changes: 2 additions & 0 deletions projects/gameboard-ui/src/app/api/game-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export interface GameDetail {
certificateTemplate: string;
externalHostId?: string;
feedbackTemplate?: FeedbackTemplate;
challengesFeedbackTemplateId?: string;
feedbackTemplateId?: string;
registrationMarkdown: string;
registrationOpen: Date;
registrationClose: Date;
Expand Down
Loading

0 comments on commit 6eb4c0e

Please sign in to comment.