Skip to content

Commit

Permalink
v3.26.2 (#212)
Browse files Browse the repository at this point in the history
* Fix bugs and allow upsert of feedback for users by devs

* Feedback report formatting

* Fix rendering bugs in feedback report

* Make old feedback config yaml visible on game center settings screen

* Fixed a rendering bug in the feedback report

* Cleanup

* Feedback Report responses dialog now clarifies when feedback hasn't been finalized

* Fix to ticket list formatting

* Revert "Fix to ticket list formatting"

This reverts commit 22dba98.

* Fix to ticket list
  • Loading branch information
sei-bstein authored Dec 13, 2024
1 parent 991b6ff commit 82c8c12
Show file tree
Hide file tree
Showing 16 changed files with 96 additions and 190 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,12 @@
<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"
[readOnly]="true" [disabled]="true"></textarea>
name="feedbackConfig" [(ngModel)]="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-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-success">{{feedbackTemplate.challenge.length}}</strong>
challenge questions
</ng-container>
</ng-container>
</div>

<button *ngIf="feedbackTemplate" type="button" class="btn btn-success ml-2 flex-basis-50"
[disabled]="!sampleConfig" [appCopyOnClick]="feedbackTemplate.content">
<button *ngIf="feedbackConfig" type="button" class="btn btn-success ml-2 flex-basis-50"
[appCopyOnClick]="boundYaml">
Copy Deprecated Template to Clipboard
</button>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { BehaviorSubject, debounceTime, tap } from 'rxjs';
import { FeedbackTemplate } from '@/api/feedback-models';
import { YamlService } from '@/services/yaml.service';
import { UnsubscriberService } from '@/services/unsubscriber.service';
import { isObject } from '@/../tools/functions';
import { YAMLParseError } from 'yaml';
import { FeedbackService } from '@/api/feedback.service';
import { ModalConfirmService } from '@/services/modal-confirm.service';

@Component({
Expand All @@ -15,7 +11,7 @@ import { ModalConfirmService } from '@/services/modal-confirm.service';
providers: [UnsubscriberService]
})
export class FeedbackEditorComponent implements OnInit {
@Input() feedbackTemplate?: FeedbackTemplate;
@Input() feedbackConfig = "";
@Output() templateChange = new EventEmitter<FeedbackTemplate | null>();

protected boundYaml = "";
Expand All @@ -25,10 +21,8 @@ export class FeedbackEditorComponent implements OnInit {
private templateChangeSubject$ = new BehaviorSubject<FeedbackTemplate | null>(null);

public constructor(
private feedbackService: FeedbackService,
private modalService: ModalConfirmService,
private unsub: UnsubscriberService,
private yamlService: YamlService) {
private unsub: UnsubscriberService) {
this.unsub.add(
this.templateChangeSubject$
.pipe(
Expand All @@ -40,14 +34,7 @@ export class FeedbackEditorComponent implements OnInit {
}

async ngOnInit() {
this.sampleConfig = await this.feedbackService.getSampleYaml() || "";

if (!this.feedbackTemplate) {
this.updateYaml("");
return;
}

this.updateYaml(this.yamlService.render(this.feedbackTemplate || ""));
this.boundYaml = this.feedbackConfig;
}

protected handleAboutFeedbackClick() {
Expand All @@ -72,95 +59,4 @@ Depending on the value of **type**, additional configuration may be required. Fo
title: "About feedback templates"
});
}

protected handlePasteSample(): void {
if (this.sampleConfig && this.boundYaml) {
this.modalService.openConfirm({
title: "Paste a sample feedback configuration",
bodyContent: `Are you sure you want to replace your current feedback configuration (**${this.feedbackTemplate?.game?.length || 0}** game questions and **${this.feedbackTemplate?.challenge?.length}** challenge questions)?`,
renderBodyAsMarkdown: true,
onConfirm: () => this.updateYaml(this.sampleConfig || "")
});

return;
}

if (this.sampleConfig) {
this.updateYaml(this.sampleConfig);
}
}

protected updateYaml(yamlConfig: string) {
// if blank, delete the configuration
if (!yamlConfig) {
this.update(undefined);
return;
}

// otherwise, only send updates when the config is valid
this.boundYaml = yamlConfig;
const feedbackTemplate = this.validateInput(yamlConfig);

if (feedbackTemplate && !this.validationMessages.length) {
try {
this.update(feedbackTemplate);
}
catch (err: any) {
this.validationMessages.push(err);
}
}
}

private update(template?: FeedbackTemplate) {
this.feedbackTemplate = template;
this.templateChangeSubject$.next(template || null);
}

private validateInput(input: string): FeedbackTemplate | undefined {
this.validationMessages = [];

let parsed: FeedbackTemplate | undefined = undefined;
const invalidYaml = "This isn't a valid YAML document. Try pasting the example configuration to get started.";

if (!input)
return parsed;

try {
parsed = this.yamlService.parse<FeedbackTemplate>(input);

if (!isObject(parsed)) {
this.validationMessages.push(invalidYaml);
return undefined;
}
}
catch (err) {
if (err instanceof YAMLParseError) {
this.validationMessages.push(invalidYaml);
return undefined;
}
}

if (parsed) {
this.validationMessages = this.validationMessages.concat(this.feedbackService.validateConfig(parsed));

// don't bother converting IDs if we're not going to pass validation
if (!this.validationMessages.length) {
// workaround for a funky thing: if the value supplied for a question's "Id" property can be evaluated
// as an integer, then the yaml library parses it as an integer, even if the typescript type is different
// (e.g. string). This just forces all IDs to be strings, which is the correct type.
if (parsed?.game)
for (const gameQuestion of parsed.game) {
gameQuestion.id = gameQuestion?.id?.toString() || gameQuestion.id;
}

if (parsed?.challenge) {
for (const challengeQuestion of parsed.challenge) {
challengeQuestion.id = challengeQuestion?.id?.toString() || challengeQuestion.id;
}
}
}
}

return parsed;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,8 @@
[(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>
<app-feedback-editor *ngIf="game.feedbackConfig"
[feedbackConfig]="game.feedbackConfig"></app-feedback-editor>
</div>

<div class="col-12 mt-5">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,7 @@ export class GameCenterSettingsComponent implements AfterViewInit {
private practiceService: PracticeService,
private route: ActivatedRoute,
private toastService: ToastService,
private unsub: UnsubscriberService,
private yamlService: YamlService) {
private unsub: UnsubscriberService) {
this.unsub.add(this.route.data.subscribe(d => this.handleGameChange(d.gameId)));
}

Expand Down Expand Up @@ -107,22 +106,6 @@ export class GameCenterSettingsComponent implements AfterViewInit {
await firstValueFrom(this.gameService.update(this.game));
}

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

if (template) {
this.game.feedbackConfig = this.yamlService.render(template);
this.game.feedbackTemplate = template;
}
else {
this.game.feedbackConfig = "";
this.game.feedbackTemplate = undefined;
}

await firstValueFrom(this.gameService.update(this.game));
}

protected async handleModeChange(event: Event) {
if (!this.game) {
throw new Error("Game is required.");
Expand Down
7 changes: 7 additions & 0 deletions projects/gameboard-ui/src/app/api/feedback-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,10 @@ export interface FeedbackTemplate {
createdByUser: SimpleEntity;
name: string;
}

export interface UpdateFeedbackTemplateRequest {
id: string;
content: string;
name: string;
helpText?: string;
}
18 changes: 15 additions & 3 deletions projects/gameboard-ui/src/app/api/feedback.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { firstValueFrom, Observable, of, Subject } from 'rxjs';
import { ConfigService } from '../utility/config.service';
import { Feedback, FeedbackQuestion, FeedbackReportDetails, FeedbackSubmissionOldAndGross, FeedbackTemplate, QuestionType } from './feedback-models';
import { Feedback, FeedbackQuestion, FeedbackReportDetails, FeedbackSubmissionOldAndGross, FeedbackTemplate, QuestionType, UpdateFeedbackTemplateRequest } from './feedback-models';
import { YamlService } from '@/services/yaml.service';
import { hasProperty } from '@/../tools/functions';
import { unique } from '@/../tools/tools';
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';
import { LogService } from '@/services/log.service';

@Injectable({ providedIn: 'root' })
export class FeedbackService {
Expand All @@ -24,13 +25,20 @@ export class FeedbackService {
constructor(
config: ConfigService,
private apiUrl: ApiUrlService,
private http: HttpClient
private http: HttpClient,
private log: LogService
) {
this.url = config.apphost + 'api';
}

public buildQuestionsFromTemplateContent(content: string): FeedbackQuestionsConfig {
return this.yamlService.parse<FeedbackQuestionsConfig>(content);
try {
return this.yamlService.parse<FeedbackQuestionsConfig>(content);
}
catch (err: any) {
this.log.logError(err);
return { questions: [] };
}
}

public async createTemplate(template: CreateFeedbackTemplate) {
Expand Down Expand Up @@ -139,6 +147,10 @@ export class FeedbackService {
return this.http.put<Feedback>(`${this.url}/feedback/submit`, model);
}

public updateTemplate(model: UpdateFeedbackTemplateRequest): Promise<FeedbackTemplateView> {
return firstValueFrom(this.http.put<FeedbackTemplateView>(`${this.url}/feedback/template/${model.id}`, model));
}

public validateConfig(config: FeedbackTemplate): string[] {
const retVal: string[] = [];

Expand Down
14 changes: 12 additions & 2 deletions projects/gameboard-ui/src/app/api/game.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { ChallengeGate } from './board-models';
import { ChangedGame, Game, GameGroup, NewGame, SessionForecast, UploadedFile } from './game-models';
import { TimeWindow } from './player-models';
import { Spec } from './spec-models';
import { YamlService } from '@/services/yaml.service';
import { FeedbackTemplate } from './feedback-models';

@Injectable({ providedIn: 'root' })
export class GameService {
Expand All @@ -23,7 +25,8 @@ export class GameService {

constructor(
private http: HttpClient,
private config: ConfigService
private config: ConfigService,
private yamlService: YamlService
) {
this.url = config.apphost + 'api';
}
Expand Down Expand Up @@ -168,7 +171,14 @@ export class GameService {

game.session = new TimeWindow(game.gameStart, game.gameEnd);
game.registration = new TimeWindow(game.registrationOpen, game.registrationClose);

try {
if (game.feedbackConfig) {
game.feedbackTemplate = this.yamlService.parse<FeedbackTemplate>(game.feedbackConfig);
}
}
catch {
game.feedbackTemplate = undefined;
}
return game;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,14 @@ <h3 *ngIf="!gameId" class="m-0 p-0 flex-grow-1">{{ctx.canManage ? 'Tickets' : 'M
</div>
<div class="text-muted">
<span class="font-weight-bold-">{{ticket.requester?.name}}</span>
&nbsp;&middot;&nbsp;
<span *ngIf="ticket.challenge" class="btn-link" [appCopyOnClick]="ticket.challenge | toSupportCode"
tooltip="Copy this support code">
{{ ticket.challenge | toSupportCode }}
</span>

<ng-container *ngIf="ticket.challenge">
&nbsp;&middot;&nbsp;
<span *ngIf="ticket.challenge" class="btn-link" [appCopyOnClick]="ticket.challenge | toSupportCode"
tooltip="Copy this support code">
{{ ticket.challenge | toSupportCode }}
</span>
</ng-container>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@
</div>

<ng-template #finalized>
<alert type="success" *ngIf="!isAdminView">
<alert type="success">
Thanks for submitting your feedback! We saved it on
<strong>{{ submission.whenFinalized | friendlyDateAndTime }}</strong>.
</alert>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AfterViewInit, Component, inject, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Component, inject, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormGroup, NgForm } from '@angular/forms';
import { DateTime } from 'luxon';
Expand Down Expand Up @@ -28,10 +28,9 @@ import { SpinnerComponent } from '@/standalone/core/components/spinner/spinner.c
templateUrl: './feedback-submission-form.component.html',
styleUrls: ['./feedback-submission-form.component.scss']
})
export class FeedbackSubmissionFormComponent implements AfterViewInit, OnInit, OnDestroy {
export class FeedbackSubmissionFormComponent implements OnInit, OnDestroy {
@Input() feedbackEntity?: FeedbackSubmissionAttachedEntity;
@Input() templateId?: string;
@Input() isAdminView = false;
@Input() isPreview = false;
@ViewChild(NgForm) form!: FormGroup;

Expand Down Expand Up @@ -66,18 +65,11 @@ export class FeedbackSubmissionFormComponent implements AfterViewInit, OnInit, O
this.autosaveInit();
}

ngAfterViewInit(): void {
}

ngOnDestroy(): void {
this.autoUpdateSub?.unsubscribe();
}

async submit(isAutoSave = false) {
if (this.isAdminView) {
return;
}

if (!this.feedbackEntity) {
throw new Error("Feedback entity is required");
}
Expand All @@ -95,6 +87,7 @@ export class FeedbackSubmissionFormComponent implements AfterViewInit, OnInit, O
feedbackTemplateId: this.templateId,
isFinalized: !isAutoSave,
responses: this.submission.responses,
userId: this.localUser.user$.value!.id
});

this.bindSubmission(updatedSubmission);
Expand Down
Loading

0 comments on commit 82c8c12

Please sign in to comment.