Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Markdown Syntax to create a quiz within a scenario #195

Merged
merged 48 commits into from
Apr 15, 2024
Merged
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
9c072dc
Add quiz-checkbox component
PhilipAB Mar 29, 2022
5baaa4f
Refactor helper text and validation
PhilipAB Mar 29, 2022
309bc16
Add question type "radio"
PhilipAB Mar 29, 2022
625ca59
Resolve linting warnings and errors (code quality)
PhilipAB Mar 29, 2022
0c41b91
Run prettier on new html and scss files
PhilipAB Mar 29, 2022
3e14ac9
Fix linting errors
PhilipAB Mar 29, 2022
21c0ab1
Fix linting errors
PhilipAB Mar 29, 2022
ec4a6e3
Make validation optional
PhilipAB Mar 30, 2022
d270c15
Improve checkbox validation
PhilipAB Mar 30, 2022
d24a2b7
Fix linting errors
PhilipAB Mar 30, 2022
5a436d2
Refactor quiz
PhilipAB Mar 31, 2022
12bdc1e
Remove empty constructor
PhilipAB Apr 7, 2022
f90d730
Remove unused variable
PhilipAB Apr 7, 2022
cd923d0
Change update method
PhilipAB Apr 7, 2022
a1a7281
Disable input for quiz questions after submission
PhilipAB Apr 7, 2022
a1e5f4a
remove ngSubmit
PhilipAB Apr 7, 2022
c648e2e
Remove now unused variable
PhilipAB Apr 7, 2022
6295b9c
submit forms
PhilipAB Apr 7, 2022
e0f49b1
Fix validation
PhilipAB Apr 7, 2022
822aeea
Only submit enabled forms
PhilipAB Apr 7, 2022
6d8fd17
Validate radio buttons after(!) hitting submit
PhilipAB Apr 7, 2022
3712bd3
Merge branch 'master' into hf-markdown-quiz
PhilipAB Aug 14, 2023
d69080e
Fix linting error/warning
PhilipAB Apr 9, 2024
4eac2e7
Merge branch 'master' into hf-markdown-quiz
PhilipAB Apr 9, 2024
7b6071f
Remove commented out code
PhilipAB Apr 9, 2024
96de8be
Rm console.log
PhilipAB Apr 9, 2024
f862bee
Add form typings
PhilipAB Apr 9, 2024
af2f00b
Run prettier
jggoebel Apr 10, 2024
239526b
Remove unneccessary code
PhilipAB Apr 10, 2024
7fda2a5
Merge branch 'hf-markdown-quiz' of https://github.com/svalabs/ui into…
PhilipAB Apr 10, 2024
49b6f3c
Add error/success message customization
PhilipAB Apr 10, 2024
0059d93
Fix radio quiz validation
PhilipAB Apr 10, 2024
b3c9cd4
Fix validation config
PhilipAB Apr 10, 2024
0db0756
Fix linting errors/warnings
PhilipAB Apr 10, 2024
f885bb6
Add optional reset button for quiz component
PhilipAB Apr 12, 2024
8b35e7f
Run prettier
PhilipAB Apr 12, 2024
85deb25
Fix linting error
PhilipAB Apr 12, 2024
f877a26
Add detailed validation for checkbox component
PhilipAB Apr 12, 2024
f9c8916
Make helper/error/success message optional
PhilipAB Apr 12, 2024
27c7d96
Add detailed validation for radio quiz type
PhilipAB Apr 12, 2024
23628d1
Fix linting error
PhilipAB Apr 12, 2024
fe26d7b
Run prettier
PhilipAB Apr 12, 2024
194461b
Disable submit button if a quiz was submitted and not reset.
PhilipAB Apr 15, 2024
c913d5a
Add default success/error msg for validation standard mode
PhilipAB Apr 15, 2024
e1bbea4
Refactoring
PhilipAB Apr 15, 2024
a36251b
Remove unused imports
PhilipAB Apr 15, 2024
893827f
Fix radio validation
PhilipAB Apr 15, 2024
8d62331
Remove empty constructor
PhilipAB Apr 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -35,6 +35,10 @@ import { AngularSplitModule } from 'angular-split';
import { HfMarkdownComponent } from './hf-markdown/hf-markdown.component';
import { PrintableComponent } from './printable/printable.component';
import { GargantuaClientFactory } from './services/gargantua.service';
import { QuizCheckboxComponent } from './quiz/quiz-checkbox.component';
import { QuizRadioComponent } from './quiz/quiz-radio.component';
import { QuizBodyComponent } from './quiz/quiz-body.component';
import { QuizComponent } from './quiz/quiz.component';
import { GuacTerminalComponent } from './scenario/guacTerminal.component';
import { IdeWindowComponent } from './scenario/ideWindow.component';
import { ContextService } from './services/context.service';
@@ -69,6 +73,7 @@ import {
eyeHideIcon,
clockIcon,
} from '@cds/core/icon';
import { QuizLabelComponent } from './quiz/quiz-label.component';

ClarityIcons.addIcons(
layersIcon,
@@ -132,6 +137,11 @@ export function jwtOptionsFactory() {
ScenarioCardComponent,
StepComponent,
CtrComponent,
QuizCheckboxComponent,
QuizRadioComponent,
QuizBodyComponent,
QuizComponent,
QuizLabelComponent,
VMClaimComponent,
AtobPipe,
HfMarkdownComponent,
@@ -155,7 +165,10 @@ export function jwtOptionsFactory() {
sanitize: false,
convertHTMLEntities: false,
},
globalParsers: [{ component: CtrComponent }],
globalParsers: [
{ component: CtrComponent },
{ component: QuizComponent },
],
}),
JwtModule.forRoot({
jwtOptionsProvider: {
13 changes: 13 additions & 0 deletions src/app/hf-markdown/hf-markdown.component.ts
Original file line number Diff line number Diff line change
@@ -104,6 +104,19 @@ export class HfMarkdownComponent implements OnChanges {
`;
},

quiz(code: string, quizTitle: string, allowedAttempts?: string) {
const tempAtts = Number(allowedAttempts);
const allowedAtts = isNaN(tempAtts) || tempAtts < 1 ? 1 : tempAtts;
return `
<quiz
quizTitle="${quizTitle}"
questionsRaw="${code}"
[allowedAtts]="${allowedAtts}"
>
</quiz>
`;
},

note(code: string, type: string, message: string) {
return `
<div class="note ${type}">
19 changes: 19 additions & 0 deletions src/app/quiz/QuestionParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { QuestionType } from './QuestionType';
import { Validation } from './Validation';

export interface QuestionParams {
questionTitle: string;
helperText: string;
questionType: QuestionType;
validation: Validation;
successMsg: string;
errorMsg: string;
}

export type QuestionParam =
| 'title'
| 'info'
| 'type'
| 'validation'
| 'successMsg'
| 'errorMsg';
6 changes: 6 additions & 0 deletions src/app/quiz/QuestionType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type QuestionType = 'radio' | 'checkbox';

export function isQuestionType(value: string): value is QuestionType {
const validValues: string[] = ['radio', 'checkbox'];
return validValues.includes(value);
}
9 changes: 9 additions & 0 deletions src/app/quiz/QuizFormGroup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { FormArray, FormControl, FormGroup } from '@angular/forms';

export type QuizCheckboxFormGroup = FormGroup<{
quiz: FormArray<FormControl<boolean>>;
}>;

export type QuizRadioFormGroup = FormGroup<{
quiz: FormControl<number | null>;
}>;
6 changes: 6 additions & 0 deletions src/app/quiz/Validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type Validation = 'none' | 'standard' | 'detailed';

export function isValidation(value: string): value is Validation {
const validValues: string[] = ['none', 'standard', 'detailed'];
return validValues.includes(value);
}
86 changes: 86 additions & 0 deletions src/app/quiz/quiz-base.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { FormGroup } from '@angular/forms';
import { ClrForm } from '@clr/angular';
import { Validation } from './Validation';
import { Component, Input, OnInit, ViewChild } from '@angular/core';

@Component({
template: '',
})
export abstract class QuizBaseComponent implements OnInit {
@Input()
public options: string;
@Input()
public helperText: string;
@Input()
public title: string;
@Input()
public validation: Validation;
@Input()
public errMsg: string;
@Input()
public successMsg: string;

@ViewChild(ClrForm, { static: true })
clrForm: ClrForm;
abstract quizForm: FormGroup;

public optionTitles: string[] = [];
public isSubmitted = false;
public validSubmission = false;
public validationEnabled: boolean;

ngOnInit(): void {
this.validationEnabled = this.validation != 'none';
this.extractQuizOptions();
this.createQuizForm();
}

// This function extracts the different possible answers to a quiz question and identifies correct answers
protected abstract extractQuizOptions(): void;

// Create the quiz form group
protected abstract createQuizForm(): void;

public submit() {
this.isSubmitted = true;
if (this.quizForm.invalid) {
this.clrForm.markAsTouched();
} else {
this.validSubmission = true;
}
this.quizForm.disable();
}

public reset() {
this.isSubmitted = false;
this.validSubmission = false;
this.quizForm.reset();
this.quizForm.enable();
}

// returns if the option at the specified index is selected
protected abstract isSelectedOption(index: number): boolean;

// returns if the option at the specified index is correct
protected abstract isCorrectOption(index: number): boolean;

// funtion for a label to determine if it should be styled as correctly selected option
public hasCorrectOptionClass(index: number): boolean {
return (
this.validation == 'detailed' &&
this.isSubmitted &&
this.isCorrectOption(index)
);
}

// funtion for a label to determine if it should be styled as incorrectly selected option
public hasIncorrectOptionClass(index: number): boolean {
return (
this.validation == 'detailed' &&
this.isSubmitted &&
!this.validSubmission &&
this.isSelectedOption(index) &&
!this.isCorrectOption(index)
);
}
}
25 changes: 25 additions & 0 deletions src/app/quiz/quiz-body.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<ng-container>
<span class="clr-subtext" *ngIf="!isSubmitted && helperText !== ''">{{
helperText
}}</span>
<clr-alert
[clrAlertType]="'danger'"
[clrAlertClosable]="false"
[clrAlertSizeSmall]="true"
*ngIf="validationEnabled && isSubmitted && !isValid && errMsg !== ''"
>
<clr-alert-item>
<span class="alert-text">{{ errMsg }}</span>
</clr-alert-item>
</clr-alert>
<clr-alert
[clrAlertType]="'success'"
[clrAlertClosable]="false"
[clrAlertSizeSmall]="true"
*ngIf="validationEnabled && isSubmitted && isValid && successMsg !== ''"
>
<clr-alert-item>
<span class="alert-text">{{ successMsg }}</span>
</clr-alert-item>
</clr-alert>
</ng-container>
3 changes: 3 additions & 0 deletions src/app/quiz/quiz-body.component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.clr-subtext {
margin-bottom: 0.3rem;
}
22 changes: 22 additions & 0 deletions src/app/quiz/quiz-body.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Component, Input } from '@angular/core';

@Component({
// eslint-disable-next-line @angular-eslint/component-selector
selector: 'quiz-body',
templateUrl: 'quiz-body.component.html',
styleUrls: ['quiz-body.component.scss'],
})
export class QuizBodyComponent {
@Input()
public helperText = '';
@Input()
public isValid: boolean;
@Input()
public isSubmitted: boolean;
@Input()
public validationEnabled: boolean;
@Input()
public errMsg = '';
@Input()
public successMsg = '';
}
26 changes: 26 additions & 0 deletions src/app/quiz/quiz-checkbox.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<form class="quiz-form" clrForm [formGroup]="quizForm">
<clr-checkbox-container [class.clr-error]="quizForm.invalid && isSubmitted">
<label>{{ title }}</label>
<clr-checkbox-wrapper
formArrayName="quiz"
*ngFor="let optionTitle of optionTitles; let i = index"
>
<input type="checkbox" clrCheckbox [formControlName]="i" />
<label>
<quiz-label
[optionTitle]="optionTitle"
[hasCorrectOptionClass]="hasCorrectOptionClass(i)"
[hasIncorrectOptionClass]="hasIncorrectOptionClass(i)"
></quiz-label>
</label>
</clr-checkbox-wrapper>
</clr-checkbox-container>
<quiz-body
[helperText]="helperText"
[isValid]="validSubmission"
[isSubmitted]="isSubmitted"
[validationEnabled]="validationEnabled"
[errMsg]="errMsg"
[successMsg]="successMsg"
></quiz-body>
</form>
12 changes: 12 additions & 0 deletions src/app/quiz/quiz-checkbox.component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
pre {
display: flex;
flex-direction: column;
}
form {
display: inherit;
flex-direction: column;
}
clr-checkbox-container {
flex-direction: column !important;
margin-top: 0;
}
92 changes: 92 additions & 0 deletions src/app/quiz/quiz-checkbox.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { Component } from '@angular/core';
import {
AbstractControl,
FormArray,
NonNullableFormBuilder,
FormControl,
ValidatorFn,
} from '@angular/forms';
import { QuizCheckboxFormGroup } from './QuizFormGroup';
import { QuizBaseComponent } from './quiz-base.component';

@Component({
// eslint-disable-next-line @angular-eslint/component-selector
selector: 'quiz-checkbox',
templateUrl: 'quiz-checkbox.component.html',
styleUrls: ['quiz-checkbox.component.scss'],
})
export class QuizCheckboxComponent extends QuizBaseComponent {
public override quizForm: QuizCheckboxFormGroup;
public requiredValues: boolean[] = [];

constructor(private fb: NonNullableFormBuilder) {
super();
}

protected override extractQuizOptions() {
this.options.split('\n- ').forEach((option: string) => {
this.optionTitles.push(option.split(':(')[0]);
const requiredValue = option.split(':(')[1].toLowerCase() === 'x)';
this.requiredValues.push(requiredValue);
});
}

protected override createQuizForm() {
if (this.validationEnabled) {
this.quizForm = this.fb.group(
{
quiz: new FormArray<FormControl<boolean>>(
[],
this.validateCheckboxes(),
),
},
{ updateOn: 'change' },
);
} else {
this.quizForm = this.fb.group(
{
quiz: new FormArray<FormControl<boolean>>([]),
},
{ updateOn: 'change' },
);
}
this.addCheckboxes();
}

private addCheckboxes() {
this.optionTitles.forEach(() =>
this.optionsFormArray.push(this.fb.control(false)),
);
}

private get optionsFormArray(): FormArray<FormControl<boolean>> {
return this.quizForm.controls.quiz;
}

private validateCheckboxes(): ValidatorFn {
return (control: AbstractControl) => {
const formArray = control as FormArray<FormControl<boolean>>;
let validatedCheckboxes = true;
formArray.controls.forEach(
(control: FormControl<boolean>, index: number) => {
validatedCheckboxes =
validatedCheckboxes && control.value === this.requiredValues[index];
},
);
if (!validatedCheckboxes) {
return {
checkboxesValidated: true,
};
}
return null;
};
}

protected override isSelectedOption(index: number): boolean {
return this.optionsFormArray.at(index).value;
}

protected override isCorrectOption(index: number): boolean {
return this.requiredValues[index];
}
}
19 changes: 19 additions & 0 deletions src/app/quiz/quiz-label.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<span
[ngClass]="{
'correct-option': hasCorrectOptionClass,
'incorrect-option': hasIncorrectOptionClass,
}"
>{{ optionTitle }}</span
>
<cds-icon
*ngIf="hasCorrectOptionClass"
shape="success-standard"
status="success"
solid="true"
></cds-icon
><cds-icon
*ngIf="hasIncorrectOptionClass"
shape="exclamation-circle"
status="danger"
solid="true"
></cds-icon>
Loading

Unchanged files with check annotations Beta

const addAccessCode = this.route.snapshot.params['accesscode'];
if (addAccessCode) {
this.userService.addAccessCode(addAccessCode).subscribe({
next: (_s: ServerResponse) => {

Check warning on line 165 in src/app/app.component.ts

GitHub Actions / Build

'_s' is defined but never used
this.accesscodes.push(addAccessCode);
this.setAccessCode(addAccessCode);
this.doHomeAccessCode(addAccessCode);
},
error: (_s: ServerResponse) => {

Check warning on line 170 in src/app/app.component.ts

GitHub Actions / Build

'_s' is defined but never used
// failure
this.doHomeAccessCodeError(addAccessCode);
},
public doSaveSettings() {
this.settingsService.update(this.settingsForm.value).subscribe({
next: (_s: ServerResponse) => {

Check warning on line 377 in src/app/app.component.ts

GitHub Actions / Build

'_s' is defined but never used
this.settingsModalOpened = false;
const theme: 'light' | 'dark' | 'system' =
this.settingsForm.controls['theme'].value;
}
}
},
error: (_s: ServerResponse) => {

Check warning on line 396 in src/app/app.component.ts

GitHub Actions / Build

'_s' is defined but never used
setTimeout(() => (this.settingsModalOpened = false), 2000);
},
});