Skip to content

Commit

Permalink
feat(QuestionBank): Add authoring support (#912)
Browse files Browse the repository at this point in the history
  • Loading branch information
hirokiterashima authored Nov 29, 2022
1 parent ebb7f87 commit fed314c
Show file tree
Hide file tree
Showing 17 changed files with 537 additions and 140 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<div class="question-rules notice-bg-bg">
<h5 fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="4px">
<span i18n>Question Bank Rules</span>
<button mat-icon-button
color="primary"
matTooltip="Add a new rule"
i18n-matTooltip
matTooltipPosition="above"
(click)="addNewRule(0)">
<mat-icon>add_circle</mat-icon>
</button>
<span fxFlex></span>
<button mat-button (click)="showHelp()" i18n>Help</button>
</h5>
<ul cdkDropList [cdkDropListData]="feedbackRules" (cdkDropListDropped)="drop($event)" cdkScrollable>
<ng-container *ngFor="let rule of feedbackRules; let ruleIndex = index; let first = first; let last = last">
<li cdkDrag>
<mat-card class="rule" fxLayout="row wrap" fxLayoutGap="8px">
<div class="text-secondary" fxLayout="column" fxLayoutAlign="start center" fxLayoutGap="22px">
<span class="mat-subheading-2">{{ruleIndex + 1}}</span>
<mat-icon cdkDragHandle title="Drag to reorder" i18n-title>drag_indicator</mat-icon>
</div>
<div fxLayout="column" fxLayoutAlign="center start" fxLayoutGap="8px" fxFlex>
<mat-form-field class="rule-input form-field-no-hint" appearance="fill">
<mat-label i18n>Expression</mat-label>
<input matInput [(ngModel)]="rule.expression" (ngModelChange)='inputChanged.next($event)' />
</mat-form-field>
<div *ngFor="let question of rule.questions; let questionIndex = index; let last = last; trackBy: customTrackBy"
class="question-item"
fxLayout="row"
fxLayoutAlign="start center"
fxLayoutGap="8px">
<mat-form-field class="rule-input form-field-no-hint question-input" appearance="fill">
<mat-label *ngIf="rule.questions.length === 1" i18n>Question</mat-label>
<mat-label *ngIf="rule.questions.length > 1" i18n>Question #{{questionIndex + 1}}</mat-label>
<textarea matInput
[(ngModel)]="rule.questions[questionIndex]"
(ngModelChange)='inputChanged.next($event)'
cdkTextareaAutosize>
</textarea>
<button mat-icon-button
matSuffix
*ngIf="rule.questions.length > 1"
matTooltip="Delete question"
i18n-matTooltip
matTooltipPosition="before"
(click)="deleteFeedbackInRule(rule, questionIndex)">
<mat-icon>clear</mat-icon>
</button>
</mat-form-field>
<button *ngIf="last"
mat-icon-button
matTooltip="Add new question"
matTooltipPosition="above"
i18n-matTooltip
(click)="addNewFeedbackToRule(rule)"
i18n>
<mat-icon>add_circle</mat-icon>
</button>
</div>
</div>
<div fxLayout="column" fxLayoutAlign="start center">
<button mat-icon-button
i18n-matTooltip
matTooltip="Delete rule"
matTooltipPosition="before"
(click)="deleteRule(ruleIndex)">
<mat-icon>clear</mat-icon>
</button>
<button *ngIf="feedbackRules.length > 1"
[disabled]="first"
mat-icon-button
i18n-matTooltip
matTooltip="Move up"
matTooltipPosition="before"
(click)="moveUp(ruleIndex)">
<mat-icon>arrow_upward</mat-icon>
</button>
<button [disabled]="last"
mat-icon-button
i18n-matTooltip
matTooltip="Move down"
matTooltipPosition="before"
(click)="moveDown(ruleIndex)">
<mat-icon>arrow_downward</mat-icon>
</button>
</div>
</mat-card>
</li>
</ng-container>
</ul>
<button *ngIf="feedbackRules.length > 0"
mat-icon-button
color="primary"
matTooltip="Add a new rule at the end"
i18n-matTooltip
matTooltipPosition="above"
(click)="addNewRule(feedbackRules.length)">
<mat-icon>add_circle</mat-icon>
</button>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
@import '~style/abstracts/variables';

.question-rules {
padding: 16px;
border-radius: $card-border-radius;
}

h5 {
margin-top: 0;
}

ul {
padding: 0;
max-height: 60vh;
overflow-y: auto;
}

li {
list-style-type: none;
}

.rule {
width: 100%;
padding: 8px;
margin-bottom: 8px;
}

.rule-input {
width: 100%;
}

.cdk-drag-handle {
cursor: move;
}

.cdk-drag-placeholder {
opacity: .4;
}

.mat-subheading-2 {
margin: 0;
}

.question-item {
width: 100%;
}

.question-input {
width: 100%;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { DragDropModule } from '@angular/cdk/drag-drop';
import { MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { QuestionBankRule } from '../../../assets/wise5/components/peerChat/peer-chat-question-bank/QuestionBankRule';
import { TeacherProjectService } from '../../../assets/wise5/services/teacherProjectService';
import { StudentTeacherCommonServicesModule } from '../../student-teacher-common-services.module';
import { EditQuestionBankRulesComponent } from './edit-question-bank-rules.component';

let component: EditQuestionBankRulesComponent;
let fixture: ComponentFixture<EditQuestionBankRulesComponent>;
let projectService: TeacherProjectService;
let nodeChangedSpy;

describe('EditQuestionBankRulesComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [EditQuestionBankRulesComponent],
imports: [
DragDropModule,
HttpClientTestingModule,
MatDialogModule,
MatIconModule,
StudentTeacherCommonServicesModule
],
providers: [TeacherProjectService]
}).compileComponents();
projectService = TestBed.inject(TeacherProjectService);
nodeChangedSpy = spyOn(projectService, 'nodeChanged');
});

beforeEach(() => {
fixture = TestBed.createComponent(EditQuestionBankRulesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

addNewFeedbackToRule();
deleteFeedbackInRule();
});

function addNewFeedbackToRule() {
describe('addNewFeedbackToRule()', () => {
it('should add new question to rule', () => {
const rule = new QuestionBankRule({ questions: ['Q1'] });
component.addNewFeedbackToRule(rule);
expect(nodeChangedSpy).toHaveBeenCalled();
expect(rule.questions.length).toEqual(2);
expect(rule.questions[1]).toEqual('');
});
});
}

function deleteFeedbackInRule() {
describe('deleteFeedbackInRule()', () => {
it('should delete specified feedback', () => {
const rule = new QuestionBankRule({ questions: ['Q1', 'Q2'] });
spyOn(window, 'confirm').and.returnValue(true);
component.deleteFeedbackInRule(rule, 0);
expect(nodeChangedSpy).toHaveBeenCalled();
expect(rule.questions.length).toEqual(1);
expect(rule.questions[0]).toEqual('Q2');
});
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Component } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { EditFeedbackRulesComponent } from '../../../assets/wise5/components/common/feedbackRule/edit-feedback-rules/edit-feedback-rules.component';
import { QuestionBankRule } from '../../../assets/wise5/components/peerChat/peer-chat-question-bank/QuestionBankRule';
import { RandomKeyService } from '../../../assets/wise5/services/randomKeyService';
import { TeacherProjectService } from '../../../assets/wise5/services/teacherProjectService';

@Component({
selector: 'edit-question-bank-rules',
templateUrl: './edit-question-bank-rules.component.html',
styleUrls: ['./edit-question-bank-rules.component.scss']
})
export class EditQuestionBankRulesComponent extends EditFeedbackRulesComponent {
constructor(protected dialog: MatDialog, protected projectService: TeacherProjectService) {
super(dialog, projectService);
}

ngOnInit(): void {
super.ngOnInit();
}

protected createNewFeedbackRule(): Partial<QuestionBankRule> {
return { id: RandomKeyService.generate(), expression: '', questions: [''] };
}

deleteRule(ruleIndex: number): void {
if (confirm($localize`Are you sure you want to delete this question rule?`)) {
this.feedbackRules.splice(ruleIndex, 1);
this.projectService.nodeChanged();
}
}

addNewFeedbackToRule(rule: Partial<QuestionBankRule>): void {
(rule.questions as string[]).push('');
this.projectService.nodeChanged();
}

deleteFeedbackInRule(rule: QuestionBankRule, feedbackIndex: number): void {
if (confirm($localize`Are you sure you want to delete this question?`)) {
(rule.questions as string[]).splice(feedbackIndex, 1);
this.projectService.nodeChanged();
}
}

customTrackBy(index: number): number {
return index;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<div class="section-container">
<div class="enable-question-bank-div">
<mat-checkbox
[checked]="componentContent.questionBank?.enabled"
(change)="toggleComponent($event)"
color="primary"
i18n>
Enable Question Bank
</mat-checkbox>
</div>
<div *ngIf="componentContent.questionBank?.enabled">
<div class="reference-component-div"
fxLayout="row wrap"
fxLayoutAlign="start center"
fxLayoutGap="20px">
<mat-label class="bold" i18n>Reference Component:</mat-label>
<edit-connected-component-node-select
[connectedComponent]="componentContent.questionBank.referenceComponent"
(connectedComponentChange)="referenceComponentNodeIdChanged($event)">
</edit-connected-component-node-select>
<edit-connected-component-component-select
[componentId]="componentId"
[connectedComponent]="componentContent.questionBank.referenceComponent"
[allowedConnectedComponentTypes]="allowedReferenceComponentTypes"
(connectedComponentChange)="saveChanges()">
</edit-connected-component-component-select>
<edit-component-peer-grouping-tag
[componentContent]="componentContent.questionBank">
</edit-component-peer-grouping-tag>
</div>
<div class="feedback-rules-div">
<edit-question-bank-rules [feedbackRules]="componentContent.questionBank.rules">
</edit-question-bank-rules>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
@import '~style/abstracts/variables', '~style/themes/default';

.prompt {
width: 100%;
}

.section-container {
padding: 16px;
border: 2px solid #dddddd;
border-radius: $card-border-radius;
margin-top: 20px;
margin-bottom: 20px;
}

.reference-component-div {
margin-top: 10px;
}

.feedback-rules-div {
margin-bottom: 20px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MatCheckboxChange, MatCheckboxModule } from '@angular/material/checkbox';
import { TeacherProjectService } from '../../../assets/wise5/services/teacherProjectService';
import { StudentTeacherCommonServicesModule } from '../../student-teacher-common-services.module';
import { EditQuestionBankComponent } from './edit-question-bank.component';

let component: EditQuestionBankComponent;
let fixture: ComponentFixture<EditQuestionBankComponent>;

describe('EditQuestionBankComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [EditQuestionBankComponent],
imports: [HttpClientTestingModule, MatCheckboxModule, StudentTeacherCommonServicesModule],
providers: [TeacherProjectService]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(EditQuestionBankComponent);
component = fixture.componentInstance;
component.componentContent = {};
fixture.detectChanges();
});
toggleComponent();
});

function toggleComponent() {
describe('toggleComponent()', () => {
it('should toggle component on when it does not exist', () => {
expect(component.componentContent.questionBank).toBeUndefined();
component.toggleComponent({ checked: true } as MatCheckboxChange);
expect(component.componentContent.questionBank.enabled).toBeTrue();
});
it('should toggle component off', () => {
component.toggleComponent({ checked: false } as MatCheckboxChange);
expect(component.componentContent.questionBank.enabled).toBeFalse();
});
});
}
Loading

0 comments on commit fed314c

Please sign in to comment.