Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
4036baa
feat(teacher-reflection): add teacher reflection service, component, …
hirokiterashima Oct 9, 2026
6e91ea9
Move Teacher Reflection Questions authoring view to Advanced > Genera…
hirokiterashima Oct 10, 2026
ca4a945
refactor: migrate component to use Angular signals for inputs and out…
hirokiterashima Oct 12, 2026
438773a
feat: add saving and saved notifications to teacher reflection answers
hirokiterashima Oct 12, 2026
1e675b9
refactor: migrate component inputs and answers to Angular signals
hirokiterashima Oct 12, 2026
405a7c9
feat: add teacher reflection component support to component summary a…
hirokiterashima Oct 12, 2026
0e7971d
feat: expand teacher reflection panel by default and add instructiona…
hirokiterashima Oct 12, 2026
269895d
refactor: remove teacher reflection component from discussion summary…
hirokiterashima Oct 12, 2026
0b5e31c
fix: change fdescribe to describe in teacher reflections component spec
hirokiterashima Oct 12, 2026
4702b0a
refactor: mock NotificationService and remove provideHttpClient in te…
hirokiterashima Oct 12, 2026
ab045ea
generate messages
hirokiterashima Oct 12, 2026
032e800
include workgroup ID when saving teacher reflection answers
hirokiterashima Oct 12, 2026
a08a1b3
add confirmation prompt for deleting teacher reflection questions and…
hirokiterashima Oct 12, 2026
183bf1a
chore: update package-lock.json dependencies
hirokiterashima Oct 12, 2026
0ae95a7
Updated messages
github-actions[bot] Oct 12, 2026
4c3c7a4
Styles: match summary design, improve accessibility, update authoring UI
breity Oct 14, 2026
02247f6
Merge branch 'develop' into feat/teacher-reflection-questions
hirokiterashima Oct 14, 2026
06720f9
fix: update teacher reflection component test assertions for prefixed…
hirokiterashima Oct 14, 2026
8a93cf8
Updated messages
github-actions[bot] Oct 14, 2026
910698c
update package lock
hirokiterashima Oct 14, 2026
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
30 changes: 15 additions & 15 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { FormsModule } from '@angular/forms';
import { EditComponentAddToNotebookButtonComponent } from '../edit-component-add-to-notebook-button/edit-component-add-to-notebook-button.component';
import { EditConnectedComponentsComponent } from '../edit-connected-components/edit-connected-components.component';
import { EditComponentSummarizerSystemPromptComponent } from '../edit-component-summarizer-system-prompt/edit-component-summarizer-system-prompt.component';
import { EditComponentTeacherReflectionsComponent } from '../edit-component-teacher-reflections/edit-component-teacher-reflections.component';

@NgModule({
imports: [
Expand All @@ -31,6 +32,7 @@ import { EditComponentSummarizerSystemPromptComponent } from '../edit-component-
EditComponentWidthComponent,
EditComponentRubricComponent,
EditComponentSummarizerSystemPromptComponent,
EditComponentTeacherReflectionsComponent,
EditComponentTagsComponent,
EditComponentConstraintsComponent,
EditComponentJsonComponent,
Expand All @@ -51,6 +53,7 @@ import { EditComponentSummarizerSystemPromptComponent } from '../edit-component-
EditComponentWidthComponent,
EditComponentRubricComponent,
EditComponentSummarizerSystemPromptComponent,
EditComponentTeacherReflectionsComponent,
EditComponentTagsComponent,
EditComponentConstraintsComponent,
EditComponentJsonComponent,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<mat-expansion-panel class="w-full !mt-4">
<mat-expansion-panel-header>
<mat-panel-title i18n>Teacher Reflection Questions</mat-panel-title>
</mat-expansion-panel-header>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<div class="flex items-center gap-2">
<span i18n
>These questions will only appear for teachers (in the Grade by Step view for this
activity in the Teacher Tools).</span
>
<button
mat-icon-button
color="primary"
type="button"
(click)="addQuestion()"
class="flex items-center gap-1"
matTooltip="Add teacher reflection question"
i18n-matTooltip
matTooltipPosition="above"
>
<mat-icon>add_circle</mat-icon>
</button>
</div>

@for (q of componentContent()?.teacher?.reflectionQuestions; track q.id; let idx = $index) {
<div class="flex items-center gap-2">
<mat-form-field appearance="fill" subscriptSizing="dynamic" class="flex-grow">
<mat-label i18n>Question {{ idx + 1 }}</mat-label>
<input
matInput
[(ngModel)]="q.text"
(ngModelChange)="onQuestionTextChanged()"
placeholder="e.g. Which, if any, response surprised you?"
i18n-placeholder
/>
</mat-form-field>
<button
mat-icon-button
color="warn"
type="button"
(click)="deleteQuestion(idx)"
matTooltip="Delete question"
i18n-matTooltip
matTooltipPosition="above"
>
<mat-icon>delete</mat-icon>
</button>
</div>
}
</div>
</div>
</mat-expansion-panel>
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { EditComponentTeacherReflectionsComponent } from './edit-component-teacher-reflections.component';
import { TeacherProjectService } from '../../../assets/wise5/services/teacherProjectService';
import { ComponentContent } from '../../../assets/wise5/common/ComponentContent';

describe('EditComponentTeacherReflectionsComponent', () => {
let component: EditComponentTeacherReflectionsComponent;
let fixture: ComponentFixture<EditComponentTeacherReflectionsComponent>;
let mockProjectService: jasmine.SpyObj<TeacherProjectService>;

beforeEach(async () => {
mockProjectService = jasmine.createSpyObj('TeacherProjectService', ['nodeChanged']);

await TestBed.configureTestingModule({
imports: [EditComponentTeacherReflectionsComponent],
providers: [{ provide: TeacherProjectService, useValue: mockProjectService }]
}).compileComponents();

fixture = TestBed.createComponent(EditComponentTeacherReflectionsComponent);
component = fixture.componentInstance;
fixture.componentRef.setInput('componentContent', {
id: 'comp1',
type: 'Discussion'
} as ComponentContent);
});

it('should create and initialize reflectionQuestions if missing', () => {
fixture.detectChanges();
expect(component).toBeTruthy();
expect(component.componentContent().teacher).toBeDefined();
expect(component.componentContent().teacher.reflectionQuestions).toEqual([]);
});

it('should add a reflection question and notify node changed', () => {
fixture.detectChanges();
component.addQuestion();

expect(component.componentContent().teacher.reflectionQuestions.length).toBe(1);
expect(component.componentContent().teacher.reflectionQuestions[0].id).toBeTruthy();
expect(component.componentContent().teacher.reflectionQuestions[0].text).toBe('');
expect(mockProjectService.nodeChanged).toHaveBeenCalledTimes(1);
});

it('should delete a reflection question and notify node changed', () => {
fixture.componentRef.setInput('componentContent', {
id: 'comp1',
type: 'Discussion',
teacher: {
reflectionQuestions: [
{ id: 'q1', text: 'First question' },
{ id: 'q2', text: 'Second question' }
]
}
} as ComponentContent);
fixture.detectChanges();
spyOn(window, 'confirm').and.returnValue(true);
component.deleteQuestion(0);

expect(component.componentContent().teacher.reflectionQuestions.length).toBe(1);
expect(component.componentContent().teacher.reflectionQuestions[0].id).toBe('q2');
expect(mockProjectService.nodeChanged).toHaveBeenCalledTimes(1);
});

it('should debounce question text changes', fakeAsync(() => {
fixture.componentRef.setInput('componentContent', {
id: 'comp1',
type: 'Discussion',
teacher: {
reflectionQuestions: [{ id: 'q1', text: 'Initial' }]
}
} as ComponentContent);
fixture.detectChanges();

component.componentContent().teacher.reflectionQuestions[0].text = 'Typing 1';
component['onQuestionTextChanged']();

expect(mockProjectService.nodeChanged).not.toHaveBeenCalled();

tick(500);
component.componentContent().teacher.reflectionQuestions[0].text = 'Typing 2';
component['onQuestionTextChanged']();

tick(1000);
expect(mockProjectService.nodeChanged).toHaveBeenCalledTimes(1);
}));

it('should emit componentChangedEvent when a question is added', () => {
fixture.detectChanges();
let emitted = false;
component.componentChangedEvent.subscribe(() => {
emitted = true;
});

component.addQuestion();

expect(emitted).toBeTrue();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import {
ChangeDetectionStrategy,
Component,
OnDestroy,
OnInit,
inject,
input,
output
} from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatTooltipModule } from '@angular/material/tooltip';
import { Subject, Subscription } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
import { ComponentContent } from '../../../assets/wise5/common/ComponentContent';
import { generateRandomKey } from '../../../assets/wise5/common/string/string';
import { TeacherProjectService } from '../../../assets/wise5/services/teacherProjectService';

@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
FormsModule,
MatButtonModule,
MatExpansionModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
MatTooltipModule
],
selector: 'edit-component-teacher-reflections',
styles: [':host { display: block; width: 100%; }'],
templateUrl: 'edit-component-teacher-reflections.component.html'
})
export class EditComponentTeacherReflectionsComponent implements OnInit, OnDestroy {
readonly componentContent = input.required<ComponentContent>();
readonly componentChangedEvent = output<void>();

protected questionTextChanged = new Subject<void>();
private projectService = inject(TeacherProjectService);
private subscriptions = new Subscription();

ngOnInit(): void {
if (!this.componentContent().teacher) {
this.componentContent().teacher = { reflectionQuestions: [] };
}
this.subscriptions.add(
this.questionTextChanged.pipe(debounceTime(1000)).subscribe(() => {
this.notifyChanged();
})
);
}

ngOnDestroy(): void {
this.subscriptions.unsubscribe();
}

addQuestion(): void {
this.componentContent().teacher.reflectionQuestions.push({
id: generateRandomKey(),
text: ''
});
this.notifyChanged();
}

deleteQuestion(index: number): void {
if (confirm($localize`Are you sure you want to delete this question?`)) {
this.componentContent().teacher.reflectionQuestions.splice(index, 1);
this.notifyChanged();
}
}

protected onQuestionTextChanged(): void {
this.questionTextChanged.next();
}

private notifyChanged(): void {
this.projectService.nodeChanged();
this.componentChangedEvent.emit();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,16 @@
<ng-container [ngTemplateOutlet]="ideasSummary" />
</div>
}
@if (hasReflectionQuestions && isSinglePeriod) {
<teacher-reflection
[nodeId]="node.id"
[componentId]="component.id"
[componentType]="component.type"
[periodId]="periodId"
[questions]="component.teacher?.reflectionQuestions || []"
class="summary"
/>
}
} @else if (component.type === 'Match') {
<ng-container [ngTemplateOutlet]="scoreSummary" />
<div class="summary-content" [class.!col-span-full]="!showScoreSummary">
Expand Down Expand Up @@ -149,6 +159,16 @@
<ng-container [ngTemplateOutlet]="ideasSummary" />
</div>
}
@if (hasReflectionQuestions && isSinglePeriod) {
<teacher-reflection
[nodeId]="node.id"
[componentId]="component.id"
[componentType]="component.type"
[periodId]="periodId"
[questions]="component.teacher?.reflectionQuestions || []"
class="summary"
/>
}
}
</div>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
MatDialogRef
} from '@angular/material/dialog';
import { DiscussionSummaryComponent } from '../../../directives/teacher-summary-display/discussion-summary/discussion-summary.component';
import { TeacherReflectionComponent } from '../../../teacher-reflection/teacher-reflection.component';

@Component({
imports: [
Expand All @@ -42,6 +43,7 @@ import { DiscussionSummaryComponent } from '../../../directives/teacher-summary-
OpenResponseAiSummaryComponent,
NgTemplateOutlet,
PeerGroupButtonComponent,
TeacherReflectionComponent,
TeacherSummaryDisplayComponent
],
selector: 'component-summary',
Expand Down Expand Up @@ -148,6 +150,14 @@ export class ComponentSummaryComponent {
panelClass: 'summary-dialog'
});
}

protected get hasReflectionQuestions(): boolean {
return (this.component.teacher?.reflectionQuestions?.length ?? 0) > 0;
}

protected get isSinglePeriod(): boolean {
return this.periodId != null && this.periodId !== -1;
}
}

@Component({
Expand Down
Loading
Loading