Skip to content

Commit

Permalink
feat(Classroom Monitor): Add save indicator for comments and scores (#…
Browse files Browse the repository at this point in the history
…1560)

Co-authored-by: Jonathan Lim-Breitbart <[email protected]>
  • Loading branch information
geoffreykwan and breity authored Dec 19, 2023
1 parent c8ca1dc commit 5986dd4
Show file tree
Hide file tree
Showing 13 changed files with 222 additions and 49 deletions.
3 changes: 3 additions & 0 deletions src/app/teacher/grading-common.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { StudentTeacherCommonModule } from '../student-teacher-common.module';
import { ComponentGradingModule } from './component-grading.module';
import { StatusIconComponent } from '../classroom-monitor/status-icon/status-icon.component';
import { NavItemProgressComponent } from '../classroom-monitor/nav-item-progress/nav-item-progress.component';
import { SaveIndicatorComponent } from '../../assets/wise5/common/save-indicator/save-indicator.component';

@NgModule({
imports: [ComponentGradingModule, IntersectionObserverModule, StudentTeacherCommonModule],
Expand All @@ -23,6 +24,7 @@ import { NavItemProgressComponent } from '../classroom-monitor/nav-item-progress
EditComponentScoreComponent,
GradingEditComponentMaxScoreComponent,
NavItemProgressComponent,
SaveIndicatorComponent,
StatusIconComponent,
WorkgroupComponentGradingComponent,
WorkgroupInfoComponent,
Expand All @@ -39,6 +41,7 @@ import { NavItemProgressComponent } from '../classroom-monitor/nav-item-progress
GradingEditComponentMaxScoreComponent,
IntersectionObserverModule,
NavItemProgressComponent,
SaveIndicatorComponent,
StatusIconComponent,
WorkgroupComponentGradingComponent,
WorkgroupInfoComponent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,24 @@ import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AnnotationService } from '../../../services/annotationService';
import { EditComponentCommentComponent } from './edit-component-comment.component';
import { StudentTeacherCommonServicesModule } from '../../../../../app/student-teacher-common-services.module';
import { MatDialogModule } from '@angular/material/dialog';
import { NotificationService } from '../../../services/notificationService';

class MockAnnotationService {
createAnnotation() {}
saveAnnotation() {}
}

let annotationService;
let annotationService: AnnotationService;
let component: EditComponentCommentComponent;
let fixture: ComponentFixture<EditComponentCommentComponent>;
let notificationService: NotificationService;

describe('EditComponentCommentComponent', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
declarations: [EditComponentCommentComponent],
providers: [{ provide: AnnotationService, useClass: MockAnnotationService }],
imports: [HttpClientTestingModule, MatDialogModule, StudentTeacherCommonServicesModule],
schemas: [NO_ERRORS_SCHEMA]
});
annotationService = TestBed.inject(AnnotationService);
notificationService = TestBed.inject(NotificationService);
fixture = TestBed.createComponent(EditComponentCommentComponent);
component = fixture.componentInstance;
fixture.detectChanges();
Expand All @@ -31,17 +30,40 @@ describe('EditComponentCommentComponent', () => {

function saveComment() {
describe('saveComment()', () => {
it('should create and save annotation', () => {
const createAnnotationSpy = spyOn(
annotationService,
'createAnnotation'
).and.callFake(() => {});
const saveAnnotationSpy = spyOn(annotationService, 'saveAnnotation').and.callFake(() => {
return new Promise(() => {});
it('creates and saves annotation', async () => {
const commentText = 'Good job.';
const annotation = createAnnotation(commentText);
const createAnnotationSpy = spyOn(annotationService, 'createAnnotation').and.callFake(() => {
return annotation;
});
component.saveComment();
const saveAnnotationSpy = spyOn(annotationService, 'saveAnnotation').and.returnValue(
Promise.resolve()
);
const notificationShowSavedMessageSpy = spyOn(notificationService, 'showSavedMessage');
component.saveComment(commentText);
expect(createAnnotationSpy).toHaveBeenCalled();
expect(saveAnnotationSpy).toHaveBeenCalled();
expect(saveAnnotationSpy).toHaveBeenCalledWith(annotation);
fixture.whenStable().then(() => {
expect(notificationShowSavedMessageSpy).toHaveBeenCalledWith('Saved comment');
});
});
});
}

function createAnnotation(value: string): any {
return {
id: null,
runId: null,
periodId: null,
fromWorkgroupId: null,
toWorkgroupId: null,
nodeId: null,
componentId: null,
studentWorkId: null,
localNotebookItemId: null,
notebookItemId: null,
type: 'comment',
data: { value: value },
clientSaveTime: null
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Component, Input } from '@angular/core';
import { Subject, Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators';
import { AnnotationService } from '../../../services/annotationService';
import { NotificationService } from '../../../services/notificationService';

@Component({
selector: 'edit-component-comment',
Expand All @@ -23,31 +24,37 @@ export class EditComponentCommentComponent {
isDirty: boolean;
subscriptions: Subscription = new Subscription();

constructor(private AnnotationService: AnnotationService) {}
constructor(
private annotationService: AnnotationService,
private notificationService: NotificationService
) {}

ngOnInit() {
this.subscriptions.add(
this.commentChanged
.pipe(
tap(() => this.setIsDirty(true)),
debounceTime(5000),
distinctUntilChanged()
debounceTime(1000),
distinctUntilChanged(),
tap(() => {
this.setIsDirty(true);
this.notificationService.showSavingMessage();
})
)
.subscribe(() => {
this.saveComment();
this.saveComment(this.comment);
})
);
}

ngOnDestroy() {
if (this.isDirty) {
this.saveComment();
this.saveComment(this.comment);
}
this.subscriptions.unsubscribe();
}

saveComment() {
const annotation = this.AnnotationService.createAnnotation(
saveComment(comment: string): void {
const annotation = this.annotationService.createAnnotation(
null,
this.runId,
this.periodId,
Expand All @@ -59,10 +66,13 @@ export class EditComponentCommentComponent {
null,
null,
'comment',
{ value: this.comment },
{ value: comment },
new Date().getTime()
);
this.AnnotationService.saveAnnotation(annotation).then(() => this.setIsDirty(false));
this.annotationService.saveAnnotation(annotation).then(() => {
this.setIsDirty(false);
this.notificationService.showSavedMessage($localize`Saved comment`);
});
}

setIsDirty(isDirty: boolean) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,24 @@ import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AnnotationService } from '../../../services/annotationService';
import { EditComponentScoreComponent } from './edit-component-score.component';
import { MatDialogModule } from '@angular/material/dialog';
import { StudentTeacherCommonServicesModule } from '../../../../../app/student-teacher-common-services.module';
import { NotificationService } from '../../../services/notificationService';

class MockAnnotationService {
createAnnotation() {}
saveAnnotation() {}
}

let annotationService;
let annotationService: AnnotationService;
let component: EditComponentScoreComponent;
let fixture: ComponentFixture<EditComponentScoreComponent>;
let notificationService: NotificationService;

describe('EditComponentScoreComponent', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
declarations: [EditComponentScoreComponent],
providers: [{ provide: AnnotationService, useClass: MockAnnotationService }],
imports: [HttpClientTestingModule, MatDialogModule, StudentTeacherCommonServicesModule],
schemas: [NO_ERRORS_SCHEMA]
});
annotationService = TestBed.inject(AnnotationService);
notificationService = TestBed.inject(NotificationService);
fixture = TestBed.createComponent(EditComponentScoreComponent);
component = fixture.componentInstance;
fixture.detectChanges();
Expand All @@ -38,14 +37,14 @@ function ngOnInit() {
}

function ngOnInit_latestAnnotationNull_SetScoreTo0() {
it('should set score to 0 when latestAnnotationScore is not set', () => {
it('sets score to 0 when latestAnnotationScore is not set', () => {
component.ngOnInit();
expect(component.score).toEqual(0);
});
}

function ngOnInit_latestAnnotationSet_SetScoreFromAnnotation() {
it('should set score to latestAnnotation.score', () => {
it('sets score to latestAnnotation.score', () => {
component.latestAnnotationScore = { data: { value: 5 } };
component.ngOnInit();
expect(component.score).toEqual(5);
Expand All @@ -54,15 +53,40 @@ function ngOnInit_latestAnnotationSet_SetScoreFromAnnotation() {

function saveScore() {
describe('saveScore()', () => {
it('should create and save annotation', () => {
const createAnnotationSpy = spyOn(
annotationService,
'createAnnotation'
).and.callFake(() => {});
const saveAnnotationSpy = spyOn(annotationService, 'saveAnnotation').and.callFake(() => {});
component.saveScore(5);
it('creates and saves annotation', async () => {
const score = 5;
const annotation = createAnnotation(score);
const createAnnotationSpy = spyOn(annotationService, 'createAnnotation').and.callFake(() => {
return annotation;
});
const saveAnnotationSpy = spyOn(annotationService, 'saveAnnotation').and.callFake(() =>
Promise.resolve()
);
const notificationShowSavedMessageSpy = spyOn(notificationService, 'showSavedMessage');
component.saveScore(score);
expect(createAnnotationSpy).toHaveBeenCalled();
expect(saveAnnotationSpy).toHaveBeenCalled();
expect(saveAnnotationSpy).toHaveBeenCalledWith(annotation);
fixture.whenStable().then(() => {
expect(notificationShowSavedMessageSpy).toHaveBeenCalledWith('Saved score');
});
});
});
}

function createAnnotation(value: number): any {
return {
id: null,
runId: null,
periodId: null,
fromWorkgroupId: null,
toWorkgroupId: null,
nodeId: null,
componentId: null,
studentWorkId: null,
localNotebookItemId: null,
notebookItemId: null,
type: 'score',
data: { value: value },
clientSaveTime: null
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Component, ElementRef, Input, ViewChild } from '@angular/core';
import { Subject, Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
import { AnnotationService } from '../../../services/annotationService';
import { NotificationService } from '../../../services/notificationService';

@Component({
selector: 'edit-component-score',
Expand All @@ -24,13 +25,17 @@ export class EditComponentScoreComponent {
scoreChanged: Subject<number> = new Subject<number>();
subscriptions: Subscription = new Subscription();

constructor(private AnnotationService: AnnotationService) {}
constructor(
private annotationService: AnnotationService,
private notificationService: NotificationService
) {}

ngOnInit() {
this.isAutoScore = this.latestAnnotationScore?.type === 'autoScore';
this.score = this.latestAnnotationScore?.data.value ?? 0;
this.subscriptions.add(
this.scoreChanged.pipe(debounceTime(1000), distinctUntilChanged()).subscribe((newScore) => {
this.notificationService.showSavingMessage();
this.saveScore(newScore);
})
);
Expand All @@ -40,8 +45,8 @@ export class EditComponentScoreComponent {
this.subscriptions.unsubscribe();
}

saveScore(score: number) {
const annotation = this.AnnotationService.createAnnotation(
saveScore(score: number): void {
const annotation = this.annotationService.createAnnotation(
null,
this.runId,
this.periodId,
Expand All @@ -56,7 +61,9 @@ export class EditComponentScoreComponent {
{ value: score },
new Date().getTime()
);
this.AnnotationService.saveAnnotation(annotation);
this.annotationService.saveAnnotation(annotation).then(() => {
this.notificationService.showSavedMessage($localize`Saved score`);
});
}

focusScoreInput() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<student-grading-tools *ngIf="showTeamTools" [workgroupId]="workgroupId">
</student-grading-tools>
<span fxFlex></span>
<save-indicator></save-indicator>
<select-period *ngIf="showPeriodSelect"></select-period>
</div>
</mat-toolbar>
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@
.toolbar__tools {
padding: 0 4px;
width: 100%;
}

save-indicator {
margin: 0 16px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<div fxLayout="row wrap" fxLayoutAlign="start center">
<mat-spinner
*ngIf="globalMessage.isProgressIndicatorVisible"
[mode]="'indeterminate'"
[diameter]="24"
>
</mat-spinner>
<span *ngIf="globalMessage.text" class="component__actions__info mat-small global-message">
{{ globalMessage.text }} {{ globalMessage.time | date: 'medium' }}
</span>
</div>
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SaveIndicatorComponent } from './save-indicator.component';
import { StudentTeacherCommonServicesModule } from '../../../../app/student-teacher-common-services.module';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { MatDialogModule } from '@angular/material/dialog';

describe('SaveIndicatorComponent', () => {
let component: SaveIndicatorComponent;
let fixture: ComponentFixture<SaveIndicatorComponent>;

beforeEach(() => {
TestBed.configureTestingModule({
declarations: [SaveIndicatorComponent],
imports: [HttpClientTestingModule, MatDialogModule, StudentTeacherCommonServicesModule]
});
fixture = TestBed.createComponent(SaveIndicatorComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Loading

0 comments on commit 5986dd4

Please sign in to comment.