diff --git a/src/assets/wise5/classroomMonitor/classroomMonitorComponents/shared/testing/ClassroomMonitorTestHelper.ts b/src/assets/wise5/classroomMonitor/classroomMonitorComponents/shared/testing/ClassroomMonitorTestHelper.ts index 5461b028e51..415f9cbf13f 100644 --- a/src/assets/wise5/classroomMonitor/classroomMonitorComponents/shared/testing/ClassroomMonitorTestHelper.ts +++ b/src/assets/wise5/classroomMonitor/classroomMonitorComponents/shared/testing/ClassroomMonitorTestHelper.ts @@ -1,4 +1,4 @@ -import { StudentProgress } from '../../../student-progress/student-progress.component'; +import { StudentProgress } from '../../../student-progress/StudentProgress'; export class ClassroomMonitorTestHelper { workgroupId1: number = 1; diff --git a/src/assets/wise5/classroomMonitor/project-location/project-location.component.html b/src/assets/wise5/classroomMonitor/project-location/project-location.component.html new file mode 100644 index 00000000000..fda70b3369e --- /dev/null +++ b/src/assets/wise5/classroomMonitor/project-location/project-location.component.html @@ -0,0 +1,21 @@ +
+ @for (segment of segments; track segment.id) { +
+ @if (segment.id === currentSegment?.id) { + + place + {{ studentProgress.nodePosition }} + +
+ } @else { +
+ } +
+ } +
diff --git a/src/assets/wise5/classroomMonitor/project-location/project-location.component.scss b/src/assets/wise5/classroomMonitor/project-location/project-location.component.scss new file mode 100644 index 00000000000..f84100715f4 --- /dev/null +++ b/src/assets/wise5/classroomMonitor/project-location/project-location.component.scss @@ -0,0 +1,5 @@ +@import "tailwindcss"; + +.segment-bar { + @apply h-3 absolute bottom-0 w-full; +} diff --git a/src/assets/wise5/classroomMonitor/project-location/project-location.component.spec.ts b/src/assets/wise5/classroomMonitor/project-location/project-location.component.spec.ts new file mode 100644 index 00000000000..265dd0216ee --- /dev/null +++ b/src/assets/wise5/classroomMonitor/project-location/project-location.component.spec.ts @@ -0,0 +1,229 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ProjectLocationComponent } from './project-location.component'; +import { ProjectService } from '../../services/projectService'; +import { StudentProgress } from '../student-progress/StudentProgress'; +import { ClassroomMonitorTestingModule } from '../classroom-monitor-testing.module'; +import { Node } from '../../common/Node'; + +describe('ProjectLocationComponent', () => { + let component: ProjectLocationComponent; + let fixture: ComponentFixture; + let projectService: ProjectService; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ClassroomMonitorTestingModule, ProjectLocationComponent] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ProjectLocationComponent); + component = fixture.componentInstance; + projectService = TestBed.inject(ProjectService); + }); + + describe('ngOnChanges()', () => { + describe('when project has multiple group nodes', () => { + beforeEach(() => { + spyOn(projectService, 'getOrderedGroupNodes').and.returnValue([ + { id: 'group1', title: 'Segment 1' }, + { id: 'group2', title: 'Segment 2' }, + { id: 'group3', title: 'Segment 3' } + ]); + spyOn(projectService, 'getParentGroup').and.returnValue({ + id: 'group2', + title: 'Segment 2' + }); + }); + + it('should set segments to group nodes', () => { + component.studentProgress = new StudentProgress({ + currentNodeId: 'node2', + nodePosition: '2.1', + positionAndTitle: '2.1: Step Title' + }); + component.ngOnChanges(); + expect(component['segments'].length).toBe(3); + expect(component['segments'][0].id).toBe('group1'); + }); + + it('should set current segment to parent group of current node', () => { + component.studentProgress = new StudentProgress({ + currentNodeId: 'node2', + nodePosition: '2.1', + positionAndTitle: '2.1: Step Title' + }); + component.ngOnChanges(); + expect(component['currentSegment'].id).toBe('group2'); + expect(projectService.getParentGroup).toHaveBeenCalledWith('node2'); + }); + }); + + describe('when project has single or no group nodes', () => { + beforeEach(() => { + spyOn(projectService, 'getOrderedGroupNodes').and.returnValue([ + { id: 'group1', title: 'Main Group' } + ]); + spyOn(projectService, 'getNode').and.returnValue({ + id: 'node2', + title: 'Step 2', + type: 'node' + } as Node); + projectService.idToOrder = { + node1: { order: 1 }, + node2: { order: 2 }, + node3: { order: 3 }, + group1: { order: 0 } + }; + spyOn(projectService, 'getNodes').and.returnValue([ + { id: 'group1', type: 'group' }, + { id: 'node1', type: 'node' }, + { id: 'node2', type: 'node' }, + { id: 'node3', type: 'node' } + ]); + }); + + it('should set segments to ordered step nodes', () => { + component.studentProgress = new StudentProgress({ + currentNodeId: 'node2', + nodePosition: '2', + positionAndTitle: '2: Step Title' + }); + component.ngOnChanges(); + expect(component['segments'].length).toBe(3); + expect(component['segments'][0].id).toBe('node1'); + expect(component['segments'][1].id).toBe('node2'); + expect(component['segments'][2].id).toBe('node3'); + }); + + it('should set current segment to current node', () => { + component.studentProgress = new StudentProgress({ + currentNodeId: 'node2', + nodePosition: '2', + positionAndTitle: '2: Step Title' + }); + component.ngOnChanges(); + expect(component['currentSegment'].id).toBe('node2'); + expect(projectService.getNode).toHaveBeenCalledWith('node2'); + }); + + it('should filter out group nodes from segments', () => { + component.studentProgress = new StudentProgress({ + currentNodeId: 'node1', + nodePosition: '1', + positionAndTitle: '1: First Step' + }); + component.ngOnChanges(); + const hasGroupNode = component['segments'].some((segment) => segment.type === 'group'); + expect(hasGroupNode).toBeFalse(); + }); + + it('should sort nodes by order', () => { + projectService.idToOrder = { + node1: { order: 3 }, + node2: { order: 1 }, + node3: { order: 2 }, + group1: { order: 0 } + }; + component.studentProgress = new StudentProgress({ + currentNodeId: 'node2', + nodePosition: '1', + positionAndTitle: '1: Step Title' + }); + component.ngOnChanges(); + expect(component['segments'][0].id).toBe('node2'); + expect(component['segments'][1].id).toBe('node3'); + expect(component['segments'][2].id).toBe('node1'); + }); + }); + }); + + describe('template rendering', () => { + beforeEach(() => { + spyOn(projectService, 'getOrderedGroupNodes').and.returnValue([ + { id: 'group1', title: 'Segment 1' }, + { id: 'group2', title: 'Segment 2' }, + { id: 'group3', title: 'Segment 3' } + ]); + spyOn(projectService, 'getParentGroup').and.returnValue({ id: 'group2', title: 'Segment 2' }); + }); + + it('should display tooltip with position and title', () => { + component.studentProgress = new StudentProgress({ + currentNodeId: 'node2', + nodePosition: '2.1', + positionAndTitle: '2.1: Step Title' + }); + component.ngOnChanges(); + fixture.detectChanges(); + expect(component.studentProgress.positionAndTitle).toBe('2.1: Step Title'); + }); + + it('should render all segments', () => { + component.studentProgress = new StudentProgress({ + currentNodeId: 'node2', + nodePosition: '2.1', + positionAndTitle: '2.1: Step Title' + }); + component.ngOnChanges(); + fixture.detectChanges(); + const segments = fixture.nativeElement.querySelectorAll('.segment'); + expect(segments.length).toBe(3); + }); + + it('should display node position and icon for current segment', () => { + component.studentProgress = new StudentProgress({ + currentNodeId: 'node2', + nodePosition: '2.1', + positionAndTitle: '2.1: Step Title' + }); + component.ngOnChanges(); + fixture.detectChanges(); + const segments = fixture.nativeElement.querySelectorAll('.segment'); + const activeSegment = segments[1]; // group2 is the second segment + expect(activeSegment.textContent.trim()).toContain('2.1'); + expect(activeSegment.querySelector('mat-icon')).toBeTruthy(); + expect(activeSegment.querySelector('mat-icon').textContent).toBe('place'); + }); + + it('should apply active style to current segment bar', () => { + component.studentProgress = new StudentProgress({ + currentNodeId: 'node2', + nodePosition: '2.1', + positionAndTitle: '2.1: Step Title' + }); + component.ngOnChanges(); + fixture.detectChanges(); + const segments = fixture.nativeElement.querySelectorAll('.segment'); + const activeSegmentBar = segments[1].querySelector('.segment-bar'); + expect(activeSegmentBar.classList.contains('primary-bg')).toBeTrue(); + }); + + it('should not display node position or icon for non-current segments', () => { + component.studentProgress = new StudentProgress({ + currentNodeId: 'node2', + nodePosition: '2.1', + positionAndTitle: '2.1: Step Title' + }); + component.ngOnChanges(); + fixture.detectChanges(); + const segments = fixture.nativeElement.querySelectorAll('.segment'); + const inactiveSegment = segments[0]; // group1 is the first segment + expect(inactiveSegment.querySelector('mat-icon')).toBeFalsy(); + expect(inactiveSegment.textContent.trim()).not.toContain('2.1'); + }); + + it('should not apply active style to non-current segment bars', () => { + component.studentProgress = new StudentProgress({ + currentNodeId: 'node2', + nodePosition: '2.1', + positionAndTitle: '2.1: Step Title' + }); + component.ngOnChanges(); + fixture.detectChanges(); + const segments = fixture.nativeElement.querySelectorAll('.segment'); + const inactiveSegmentBar = segments[0].querySelector('.segment-bar'); + expect(inactiveSegmentBar.classList.contains('primary-bg')).toBeFalse(); + }); + }); +}); diff --git a/src/assets/wise5/classroomMonitor/project-location/project-location.component.ts b/src/assets/wise5/classroomMonitor/project-location/project-location.component.ts new file mode 100644 index 00000000000..069033de763 --- /dev/null +++ b/src/assets/wise5/classroomMonitor/project-location/project-location.component.ts @@ -0,0 +1,38 @@ +import { Component, inject, Input } from '@angular/core'; +import { MatTooltip } from '@angular/material/tooltip'; +import { ProjectService } from '../../services/projectService'; +import { MatIcon } from '@angular/material/icon'; +import { StudentProgress } from '../student-progress/StudentProgress'; + +@Component({ + imports: [MatIcon, MatTooltip], + selector: 'project-location', + styleUrl: './project-location.component.scss', + templateUrl: './project-location.component.html' +}) +export class ProjectLocationComponent { + private projectService: ProjectService = inject(ProjectService); + + protected currentSegment: any; + protected segments: any[]; + @Input() studentProgress: StudentProgress; + + ngOnChanges(): void { + const groupNodes = this.projectService.getOrderedGroupNodes(); + if (groupNodes.length > 1) { + this.segments = groupNodes; + this.currentSegment = this.projectService.getParentGroup(this.studentProgress.currentNodeId); + } else { + this.segments = this.getOrderedNodes(); + this.currentSegment = this.projectService.getNode(this.studentProgress.currentNodeId); + } + } + + private getOrderedNodes(): any[] { + const idToOrder = this.projectService.idToOrder; + return this.projectService + .getNodes() + .filter((node) => node.type !== 'group') + .sort((a, b) => idToOrder[a.id].order - idToOrder[b.id].order); + } +} diff --git a/src/assets/wise5/classroomMonitor/student-progress/StudentProgress.ts b/src/assets/wise5/classroomMonitor/student-progress/StudentProgress.ts new file mode 100644 index 00000000000..4dd3dd5dbcf --- /dev/null +++ b/src/assets/wise5/classroomMonitor/student-progress/StudentProgress.ts @@ -0,0 +1,25 @@ +import { ProjectCompletion } from '../../common/ProjectCompletion'; + +export class StudentProgress { + currentNodeId: string; + periodId: string; + periodName: string; + workgroupId: number; + username: string; + firstName: string; + lastName: string; + nodePosition: string; + positionAndTitle: string; + order: number; + completion: ProjectCompletion; + completionPct: number; + score: number; + maxScore: number; + scorePct: number; + + constructor(jsonObject: any = {}) { + for (const key of Object.keys(jsonObject)) { + this[key] = jsonObject[key]; + } + } +} diff --git a/src/assets/wise5/classroomMonitor/student-progress/student-progress.component.html b/src/assets/wise5/classroomMonitor/student-progress/student-progress.component.html index 07d770d9eed..8cfb85cc302 100644 --- a/src/assets/wise5/classroomMonitor/student-progress/student-progress.component.html +++ b/src/assets/wise5/classroomMonitor/student-progress/student-progress.component.html @@ -54,7 +54,9 @@ } @else { {{ student.username }} } - {{ student.position }} + + + { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ClassroomMonitorTestingModule, StudentProgressComponent], + imports: [ + ClassroomMonitorTestingModule, + MockComponent(ProjectLocationComponent), + StudentProgressComponent + ], providers: [provideRouter([])] }).compileComponents(); }); diff --git a/src/assets/wise5/classroomMonitor/student-progress/student-progress.component.ts b/src/assets/wise5/classroomMonitor/student-progress/student-progress.component.ts index 73021440642..be589940da1 100644 --- a/src/assets/wise5/classroomMonitor/student-progress/student-progress.component.ts +++ b/src/assets/wise5/classroomMonitor/student-progress/student-progress.component.ts @@ -4,7 +4,6 @@ import { ConfigService } from '../../services/configService'; import { ClassroomStatusService } from '../../services/classroomStatusService'; import { TeacherDataService } from '../../services/teacherDataService'; import { ActivatedRoute, Router } from '@angular/router'; -import { ProjectCompletion } from '../../common/ProjectCompletion'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; @@ -12,6 +11,8 @@ import { MatTableModule } from '@angular/material/table'; import { CommonModule } from '@angular/common'; import { WorkgroupSelectAutocompleteComponent } from '../../../../app/classroom-monitor/workgroup-select/workgroup-select-autocomplete/workgroup-select-autocomplete.component'; import { ProjectProgressComponent } from '../classroomMonitorComponents/studentProgress/project-progress/project-progress.component'; +import { ProjectLocationComponent } from '../project-location/project-location.component'; +import { StudentProgress } from './StudentProgress'; @Component({ encapsulation: ViewEncapsulation.None, @@ -21,6 +22,7 @@ import { ProjectProgressComponent } from '../classroomMonitorComponents/studentP MatIconModule, MatListModule, MatTableModule, + ProjectLocationComponent, ProjectProgressComponent, WorkgroupSelectAutocompleteComponent ], @@ -129,7 +131,9 @@ export class StudentProgressComponent implements OnInit { this.students .filter((student) => student.workgroupId === workgroupId) .forEach((student) => { - student.position = location?.position || ''; + student.currentNodeId = location?.nodeId || ''; + student.nodePosition = location?.nodePosition || ''; + student.positionAndTitle = location?.positionAndTitle || ''; student.order = location?.order || 0; student.completion = completion; student.completionPct = completion.completionPct || 0; @@ -200,25 +204,3 @@ export class StudentProgressComponent implements OnInit { this.sortWorkgroups(); } } - -export class StudentProgress { - periodId: string; - periodName: string; - workgroupId: number; - username: string; - firstName: string; - lastName: string; - position: string; - order: number; - completion: ProjectCompletion; - completionPct: number; - score: number; - maxScore: number; - scorePct: number; - - constructor(jsonObject: any = {}) { - for (const key of Object.keys(jsonObject)) { - this[key] = jsonObject[key]; - } - } -} diff --git a/src/assets/wise5/services/classroomStatusService.ts b/src/assets/wise5/services/classroomStatusService.ts index 7e2f7c0b4fc..a52909c159e 100644 --- a/src/assets/wise5/services/classroomStatusService.ts +++ b/src/assets/wise5/services/classroomStatusService.ts @@ -56,7 +56,9 @@ export class ClassroomStatusService { if (studentStatus != null) { const currentNodeId = studentStatus.currentNodeId; return { - position: this.projectService.getNodePositionAndTitle(currentNodeId), + nodeId: currentNodeId, + nodePosition: this.projectService.getNodePositionById(currentNodeId), + positionAndTitle: this.projectService.getNodePositionAndTitle(currentNodeId), order: this.projectService.getNodeOrderById(currentNodeId) }; } diff --git a/src/messages.xlf b/src/messages.xlf index 57fb072b317..ab58ef47772 100644 --- a/src/messages.xlf +++ b/src/messages.xlf @@ -2701,7 +2701,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/assets/wise5/classroomMonitor/student-progress/student-progress.component.ts - 68 + 70 src/assets/wise5/components/conceptMap/concept-map-student/concept-map-student.component.ts @@ -3201,7 +3201,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/assets/wise5/classroomMonitor/student-progress/student-progress.component.ts - 43 + 45 @@ -3444,7 +3444,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/assets/wise5/classroomMonitor/student-progress/student-progress.component.ts - 48 + 50 @@ -3498,7 +3498,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/assets/wise5/classroomMonitor/student-progress/student-progress.component.ts - 53 + 55 @@ -14359,7 +14359,7 @@ The branches will be removed but the steps will remain in the unit. src/assets/wise5/classroomMonitor/student-progress/student-progress.component.ts - 38 + 40 @@ -15851,7 +15851,7 @@ Are you sure you want to proceed? src/assets/wise5/classroomMonitor/student-progress/student-progress.component.ts - 63 + 65 @@ -15897,7 +15897,7 @@ Are you sure you want to proceed? Location src/assets/wise5/classroomMonitor/student-progress/student-progress.component.ts - 58 + 60