diff --git a/src/app/classroom-monitor/teams-on-node/teams-on-node.component.html b/src/app/classroom-monitor/teams-on-node/teams-on-node.component.html
new file mode 100644
index 00000000000..75a187a7991
--- /dev/null
+++ b/src/app/classroom-monitor/teams-on-node/teams-on-node.component.html
@@ -0,0 +1,13 @@
+@if (workgroupsOnNode.length > 0) {
+
+
+ person
+ {{ workgroupsOnNode.length }}
+
+
+}
diff --git a/src/app/classroom-monitor/teams-on-node/teams-on-node.component.scss b/src/app/classroom-monitor/teams-on-node/teams-on-node.component.scss
new file mode 100644
index 00000000000..320cc8e058f
--- /dev/null
+++ b/src/app/classroom-monitor/teams-on-node/teams-on-node.component.scss
@@ -0,0 +1,3 @@
+.mdc-tooltip--multiline .mat-mdc-tooltip-surface {
+ white-space: break-spaces;
+}
\ No newline at end of file
diff --git a/src/app/classroom-monitor/teams-on-node/teams-on-node.component.spec.ts b/src/app/classroom-monitor/teams-on-node/teams-on-node.component.spec.ts
new file mode 100644
index 00000000000..0bb0e7d8ace
--- /dev/null
+++ b/src/app/classroom-monitor/teams-on-node/teams-on-node.component.spec.ts
@@ -0,0 +1,308 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { TeamsOnNodeComponent } from './teams-on-node.component';
+import { ClassroomStatusService } from '../../../assets/wise5/services/classroomStatusService';
+import { ConfigService } from '../../../assets/wise5/services/configService';
+import { ProjectService } from '../../../assets/wise5/services/projectService';
+import { Subject } from 'rxjs';
+
+describe('TeamsOnNodeComponent', () => {
+ let component: TeamsOnNodeComponent;
+ let fixture: ComponentFixture;
+ let classroomStatusService: jasmine.SpyObj;
+ let configService: jasmine.SpyObj;
+ let projectService: jasmine.SpyObj;
+ let studentStatusReceivedSubject: Subject;
+
+ const nodeId = 'node1';
+ const stepNodeId = 'node2';
+ const lessonNodeId = 'group1';
+ const periodId = 1;
+ const periodName = 'Period 1';
+
+ const createMockWorkgroup = (workgroupId: number) => ({
+ workgroupId,
+ periodId
+ });
+
+ beforeEach(async () => {
+ studentStatusReceivedSubject = new Subject();
+ const classroomStatusServiceSpy = jasmine.createSpyObj('ClassroomStatusService', [
+ 'getWorkgroupsOnNode'
+ ]);
+ classroomStatusServiceSpy.studentStatusReceived$ = studentStatusReceivedSubject.asObservable();
+ const configServiceSpy = jasmine.createSpyObj('ConfigService', [
+ 'getPermissions',
+ 'getDisplayUsernamesByWorkgroupId'
+ ]);
+ const projectServiceSpy = jasmine.createSpyObj('ProjectService', ['isApplicationNode']);
+
+ await TestBed.configureTestingModule({
+ imports: [TeamsOnNodeComponent],
+ providers: [
+ { provide: ClassroomStatusService, useValue: classroomStatusServiceSpy },
+ { provide: ConfigService, useValue: configServiceSpy },
+ { provide: ProjectService, useValue: projectServiceSpy }
+ ]
+ }).compileComponents();
+
+ classroomStatusService = TestBed.inject(
+ ClassroomStatusService
+ ) as jasmine.SpyObj;
+ configService = TestBed.inject(ConfigService) as jasmine.SpyObj;
+ projectService = TestBed.inject(ProjectService) as jasmine.SpyObj;
+
+ fixture = TestBed.createComponent(TeamsOnNodeComponent);
+ component = fixture.componentInstance;
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('ngOnInit', () => {
+ it('should subscribe to studentStatusReceived$ and call ngOnChanges', () => {
+ component.nodeId = nodeId;
+ component.period = { periodId, periodName };
+ classroomStatusService.getWorkgroupsOnNode.and.returnValue([]);
+ projectService.isApplicationNode.and.returnValue(true);
+ configService.getPermissions.and.returnValue({ canViewStudentNames: false } as any);
+
+ fixture.detectChanges();
+
+ spyOn(component, 'ngOnChanges');
+ studentStatusReceivedSubject.next();
+
+ expect(component.ngOnChanges).toHaveBeenCalled();
+ });
+ });
+
+ describe('ngOnDestroy', () => {
+ it('should unsubscribe from subscriptions', () => {
+ component.nodeId = nodeId;
+ component.period = { periodId, periodName };
+ classroomStatusService.getWorkgroupsOnNode.and.returnValue([]);
+ projectService.isApplicationNode.and.returnValue(true);
+ configService.getPermissions.and.returnValue({ canViewStudentNames: false } as any);
+
+ fixture.detectChanges();
+
+ spyOn(component['subscriptions'], 'unsubscribe');
+
+ component.ngOnDestroy();
+
+ expect(component['subscriptions'].unsubscribe).toHaveBeenCalled();
+ });
+ });
+
+ describe('ngOnChanges', () => {
+ describe('workgroups on node', () => {
+ it('should get workgroups on node for the specified period', () => {
+ const workgroups = [createMockWorkgroup(1), createMockWorkgroup(2)];
+ component.nodeId = nodeId;
+ component.period = { periodId, periodName };
+ classroomStatusService.getWorkgroupsOnNode.and.returnValue(workgroups);
+ projectService.isApplicationNode.and.returnValue(true);
+ configService.getPermissions.and.returnValue({ canViewStudentNames: false } as any);
+
+ component.ngOnChanges();
+
+ expect(classroomStatusService.getWorkgroupsOnNode).toHaveBeenCalledWith(nodeId, periodId);
+ expect(component['workgroupsOnNode']).toEqual(workgroups);
+ });
+ });
+
+ describe('tooltip text for step', () => {
+ beforeEach(() => {
+ component.nodeId = stepNodeId;
+ projectService.isApplicationNode.and.returnValue(true);
+ configService.getPermissions.and.returnValue({ canViewStudentNames: false } as any);
+ });
+
+ it('should create tooltip text for single team on step in specific period', () => {
+ const workgroups = [createMockWorkgroup(1)];
+ component.period = { periodId, periodName };
+ classroomStatusService.getWorkgroupsOnNode.and.returnValue(workgroups);
+
+ component.ngOnChanges();
+
+ expect(component['tooltipText']).toBe('1 team on this step (Period: Period 1)');
+ });
+
+ it('should create tooltip text for multiple teams on step in specific period', () => {
+ const workgroups = [createMockWorkgroup(1), createMockWorkgroup(2), createMockWorkgroup(3)];
+ component.period = { periodId, periodName };
+ classroomStatusService.getWorkgroupsOnNode.and.returnValue(workgroups);
+
+ component.ngOnChanges();
+
+ expect(component['tooltipText']).toBe('3 teams on this step (Period: Period 1)');
+ });
+
+ it('should create tooltip text for single team on step in all periods', () => {
+ const workgroups = [createMockWorkgroup(1)];
+ component.period = { periodId: -1, periodName: 'All Periods' };
+ classroomStatusService.getWorkgroupsOnNode.and.returnValue(workgroups);
+
+ component.ngOnChanges();
+
+ expect(component['tooltipText']).toBe('1 team on this step (All periods)');
+ });
+
+ it('should create tooltip text for multiple teams on step in all periods', () => {
+ const workgroups = [createMockWorkgroup(1), createMockWorkgroup(2)];
+ component.period = { periodId: -1, periodName: 'All Periods' };
+ classroomStatusService.getWorkgroupsOnNode.and.returnValue(workgroups);
+
+ component.ngOnChanges();
+
+ expect(component['tooltipText']).toBe('2 teams on this step (All periods)');
+ });
+ });
+
+ describe('tooltip text for lesson', () => {
+ beforeEach(() => {
+ component.nodeId = lessonNodeId;
+ projectService.isApplicationNode.and.returnValue(false);
+ configService.getPermissions.and.returnValue({ canViewStudentNames: false } as any);
+ });
+
+ it('should create tooltip text for single team on lesson in specific period', () => {
+ const workgroups = [createMockWorkgroup(1)];
+ component.period = { periodId, periodName };
+ classroomStatusService.getWorkgroupsOnNode.and.returnValue(workgroups);
+
+ component.ngOnChanges();
+
+ expect(component['tooltipText']).toBe('1 team on this lesson (Period: Period 1)');
+ });
+
+ it('should create tooltip text for multiple teams on lesson in specific period', () => {
+ const workgroups = [createMockWorkgroup(1), createMockWorkgroup(2)];
+ component.period = { periodId, periodName };
+ classroomStatusService.getWorkgroupsOnNode.and.returnValue(workgroups);
+
+ component.ngOnChanges();
+
+ expect(component['tooltipText']).toBe('2 teams on this lesson (Period: Period 1)');
+ });
+
+ it('should create tooltip text for single team on lesson in all periods', () => {
+ const workgroups = [createMockWorkgroup(1)];
+ component.period = { periodId: -1, periodName: 'All Periods' };
+ classroomStatusService.getWorkgroupsOnNode.and.returnValue(workgroups);
+
+ component.ngOnChanges();
+
+ expect(component['tooltipText']).toBe('1 team on this lesson (All periods)');
+ });
+
+ it('should create tooltip text for multiple teams on lesson in all periods', () => {
+ const workgroups = [createMockWorkgroup(1), createMockWorkgroup(2), createMockWorkgroup(3)];
+ component.period = { periodId: -1, periodName: 'All Periods' };
+ classroomStatusService.getWorkgroupsOnNode.and.returnValue(workgroups);
+
+ component.ngOnChanges();
+
+ expect(component['tooltipText']).toBe('3 teams on this lesson (All periods)');
+ });
+ });
+
+ describe('tooltip text with student names', () => {
+ beforeEach(() => {
+ component.nodeId = stepNodeId;
+ component.period = { periodId, periodName };
+ projectService.isApplicationNode.and.returnValue(true);
+ configService.getPermissions.and.returnValue({ canViewStudentNames: true } as any);
+ });
+
+ it('should append student names to tooltip when permission is granted', () => {
+ const workgroups = [createMockWorkgroup(1), createMockWorkgroup(2)];
+ classroomStatusService.getWorkgroupsOnNode.and.returnValue(workgroups);
+ configService.getDisplayUsernamesByWorkgroupId.and.callFake((workgroupId: number) => {
+ if (workgroupId === 1) return 'Alice, Bob';
+ if (workgroupId === 2) return 'Charlie, David';
+ return '';
+ });
+
+ component.ngOnChanges();
+
+ expect(component['tooltipText']).toBe(
+ '2 teams on this step (Period: Period 1)\nAlice, Bob\nCharlie, David\n'
+ );
+ });
+
+ it('should not append student names when permission is not granted', () => {
+ const workgroups = [createMockWorkgroup(1), createMockWorkgroup(2)];
+ classroomStatusService.getWorkgroupsOnNode.and.returnValue(workgroups);
+ configService.getPermissions.and.returnValue({ canViewStudentNames: false } as any);
+
+ component.ngOnChanges();
+
+ expect(component['tooltipText']).toBe('2 teams on this step (Period: Period 1)');
+ expect(configService.getDisplayUsernamesByWorkgroupId).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('template rendering', () => {
+ it('should display workgroup count when there are workgroups on node', () => {
+ const workgroups = [createMockWorkgroup(1), createMockWorkgroup(2), createMockWorkgroup(3)];
+ component.nodeId = nodeId;
+ component.period = { periodId, periodName };
+ classroomStatusService.getWorkgroupsOnNode.and.returnValue(workgroups);
+ projectService.isApplicationNode.and.returnValue(true);
+ configService.getPermissions.and.returnValue({ canViewStudentNames: false } as any);
+
+ component.ngOnChanges();
+ fixture.detectChanges();
+
+ const countElement = fixture.nativeElement.querySelector('.progress-wrapper');
+ expect(countElement).toBeTruthy();
+ expect(countElement.textContent.trim()).toContain('3');
+ });
+
+ it('should not display anything when there are no workgroups on node', () => {
+ component.nodeId = nodeId;
+ component.period = { periodId, periodName };
+ classroomStatusService.getWorkgroupsOnNode.and.returnValue([]);
+ projectService.isApplicationNode.and.returnValue(true);
+ configService.getPermissions.and.returnValue({ canViewStudentNames: false } as any);
+
+ component.ngOnChanges();
+ fixture.detectChanges();
+
+ const countElement = fixture.nativeElement.querySelector('.progress-wrapper');
+ expect(countElement).toBeFalsy();
+ });
+
+ it('should display mat-icon with person icon', () => {
+ const workgroups = [createMockWorkgroup(1)];
+ component.nodeId = nodeId;
+ component.period = { periodId, periodName };
+ classroomStatusService.getWorkgroupsOnNode.and.returnValue(workgroups);
+ projectService.isApplicationNode.and.returnValue(true);
+ configService.getPermissions.and.returnValue({ canViewStudentNames: false } as any);
+
+ component.ngOnChanges();
+ fixture.detectChanges();
+
+ const iconElement = fixture.nativeElement.querySelector('mat-icon');
+ expect(iconElement).toBeTruthy();
+ expect(iconElement.textContent.trim()).toBe('person');
+ });
+
+ it('should set tooltip text for mat-icon', () => {
+ const workgroups = [createMockWorkgroup(1), createMockWorkgroup(2)];
+ component.nodeId = stepNodeId;
+ component.period = { periodId, periodName };
+ classroomStatusService.getWorkgroupsOnNode.and.returnValue(workgroups);
+ projectService.isApplicationNode.and.returnValue(true);
+ configService.getPermissions.and.returnValue({ canViewStudentNames: false } as any);
+
+ component.ngOnChanges();
+ fixture.detectChanges();
+
+ expect(component['tooltipText']).toBe('2 teams on this step (Period: Period 1)');
+ });
+ });
+});
diff --git a/src/app/classroom-monitor/teams-on-node/teams-on-node.component.ts b/src/app/classroom-monitor/teams-on-node/teams-on-node.component.ts
new file mode 100644
index 00000000000..1cc9e9b3376
--- /dev/null
+++ b/src/app/classroom-monitor/teams-on-node/teams-on-node.component.ts
@@ -0,0 +1,64 @@
+import { Component, inject, Input, ViewEncapsulation } from '@angular/core';
+import { MatTooltipModule } from '@angular/material/tooltip';
+import { ClassroomStatusService } from '../../../assets/wise5/services/classroomStatusService';
+import { MatIconModule } from '@angular/material/icon';
+import { ProjectService } from '../../../assets/wise5/services/projectService';
+import { ConfigService } from '../../../assets/wise5/services/configService';
+import { Subscription } from 'rxjs';
+
+@Component({
+ encapsulation: ViewEncapsulation.None,
+ imports: [MatIconModule, MatTooltipModule],
+ selector: 'teams-on-node',
+ styleUrl: './teams-on-node.component.scss',
+ templateUrl: './teams-on-node.component.html'
+})
+export class TeamsOnNodeComponent {
+ private classroomStatusService = inject(ClassroomStatusService);
+ private configService = inject(ConfigService);
+ private projectService = inject(ProjectService);
+
+ @Input() nodeId: string;
+ @Input() period: any;
+ private subscriptions: Subscription = new Subscription();
+ protected tooltipText: string;
+ protected workgroupsOnNode: any[] = [];
+
+ ngOnInit(): void {
+ this.subscriptions.add(
+ this.classroomStatusService.studentStatusReceived$.subscribe(() => {
+ this.ngOnChanges();
+ })
+ );
+ }
+
+ ngOnDestroy(): void {
+ this.subscriptions.unsubscribe();
+ }
+
+ ngOnChanges(): void {
+ this.workgroupsOnNode = this.classroomStatusService.getWorkgroupsOnNode(
+ this.nodeId,
+ this.period.periodId
+ );
+ const teams = this.workgroupsOnNode.length === 1 ? $localize`team` : $localize`teams`;
+ const stepOrLesson = this.projectService.isApplicationNode(this.nodeId)
+ ? $localize`step`
+ : $localize`lesson`;
+ if (this.period.periodId === -1) {
+ this.tooltipText = $localize`${this.workgroupsOnNode.length} ${teams} on this ${stepOrLesson} (All periods)`;
+ } else {
+ this.tooltipText = $localize`${this.workgroupsOnNode.length} ${teams} on this ${stepOrLesson} (Period: ${this.period.periodName})`;
+ }
+ if (this.configService.getPermissions().canViewStudentNames) {
+ this.tooltipText +=
+ `\n` +
+ this.workgroupsOnNode
+ .map(
+ (workgroup) =>
+ `${this.configService.getDisplayUsernamesByWorkgroupId(workgroup.workgroupId)}\n`
+ )
+ .join('');
+ }
+ }
+}
diff --git a/src/assets/wise5/classroomMonitor/classroomMonitorComponents/nodeProgress/nav-item/nav-item.component.html b/src/assets/wise5/classroomMonitor/classroomMonitorComponents/nodeProgress/nav-item/nav-item.component.html
index 0852c1b87e6..8c7ffcfbf18 100644
--- a/src/assets/wise5/classroomMonitor/classroomMonitorComponents/nodeProgress/nav-item/nav-item.component.html
+++ b/src/assets/wise5/classroomMonitor/classroomMonitorComponents/nodeProgress/nav-item/nav-item.component.html
@@ -46,6 +46,7 @@
+
}
+
{
beforeEach(async () => {
await TestBed.configureTestingModule({
- imports: [ClassroomMonitorTestingModule, NavItemComponent],
+ imports: [
+ ClassroomMonitorTestingModule,
+ NavItemComponent,
+ MockComponent(TeamsOnNodeComponent)
+ ],
providers: [
{ provide: NodeService, useClass: MockNodeService },
{ provide: NotificationService, useClass: MockNotificationService },
diff --git a/src/assets/wise5/classroomMonitor/classroomMonitorComponents/nodeProgress/nav-item/nav-item.component.ts b/src/assets/wise5/classroomMonitor/classroomMonitorComponents/nodeProgress/nav-item/nav-item.component.ts
index a6901f0f6a3..79c3a4fc9eb 100644
--- a/src/assets/wise5/classroomMonitor/classroomMonitorComponents/nodeProgress/nav-item/nav-item.component.ts
+++ b/src/assets/wise5/classroomMonitor/classroomMonitorComponents/nodeProgress/nav-item/nav-item.component.ts
@@ -21,6 +21,7 @@ import { NodeIconComponent } from '../../../../vle/node-icon/node-icon.component
import { NavItemScoreComponent } from '../navItemScore/nav-item-score.component';
import { MatTooltipModule } from '@angular/material/tooltip';
import { CommonModule } from '@angular/common';
+import { TeamsOnNodeComponent } from '../../../../../../app/classroom-monitor/teams-on-node/teams-on-node.component';
@Component({
imports: [
@@ -34,7 +35,8 @@ import { CommonModule } from '@angular/common';
NavItemProgressComponent,
NavItemScoreComponent,
NodeIconComponent,
- StatusIconComponent
+ StatusIconComponent,
+ TeamsOnNodeComponent
],
selector: 'nav-item',
styleUrl: './nav-item.component.scss',
diff --git a/src/assets/wise5/services/classroomStatusService.ts b/src/assets/wise5/services/classroomStatusService.ts
index a9c59f4a767..7e2f7c0b4fc 100644
--- a/src/assets/wise5/services/classroomStatusService.ts
+++ b/src/assets/wise5/services/classroomStatusService.ts
@@ -358,4 +358,18 @@ export class ClassroomStatusService {
broadcastStudentStatusReceived(args: any) {
this.studentStatusReceivedSource.next(args);
}
+
+ getWorkgroupsOnNode(nodeId: string, periodId: number): any[] {
+ return this.studentStatuses.filter(
+ (status) =>
+ (status.currentNodeId === nodeId ||
+ (this.projectService.isGroupNode(nodeId) &&
+ this.isNodeInGroup(status.currentNodeId, nodeId))) &&
+ this.periodMatches(status, periodId)
+ );
+ }
+
+ private isNodeInGroup(nodeId: string, groupId: string): boolean {
+ return this.projectService.getNodeById(groupId).ids.includes(nodeId);
+ }
}
diff --git a/src/messages.xlf b/src/messages.xlf
index 7ec3d98f169..47c245442ec 100644
--- a/src/messages.xlf
+++ b/src/messages.xlf
@@ -2061,7 +2061,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.
src/assets/wise5/classroomMonitor/classroomMonitorComponents/nodeProgress/nav-item/nav-item.component.ts
- 98
+ 100
@@ -2083,7 +2083,49 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.
src/assets/wise5/classroomMonitor/classroomMonitorComponents/nodeProgress/nav-item/nav-item.component.ts
- 101
+ 103
+
+
+
+ team
+
+ src/app/classroom-monitor/teams-on-node/teams-on-node.component.ts
+ 44
+
+
+
+ teams
+
+ src/app/classroom-monitor/teams-on-node/teams-on-node.component.ts
+ 44
+
+
+
+ step
+
+ src/app/classroom-monitor/teams-on-node/teams-on-node.component.ts
+ 46
+
+
+
+ lesson
+
+ src/app/classroom-monitor/teams-on-node/teams-on-node.component.ts
+ 47
+
+
+
+ on this (All periods)
+
+ src/app/classroom-monitor/teams-on-node/teams-on-node.component.ts
+ 49
+
+
+
+ on this (Period: )
+
+ src/app/classroom-monitor/teams-on-node/teams-on-node.component.ts
+ 51
@@ -14725,21 +14767,21 @@ The branches will be removed but the steps will remain in the unit.
has been locked for .
src/assets/wise5/classroomMonitor/classroomMonitorComponents/nodeProgress/nav-item/nav-item.component.ts
- 252
+ 254
has been unlocked for .
src/assets/wise5/classroomMonitor/classroomMonitorComponents/nodeProgress/nav-item/nav-item.component.ts
- 253
+ 255
All Periods
src/assets/wise5/classroomMonitor/classroomMonitorComponents/nodeProgress/nav-item/nav-item.component.ts
- 379
+ 381
src/assets/wise5/classroomMonitor/classroomMonitorComponents/select-period/select-period.component.html
@@ -14758,7 +14800,7 @@ The branches will be removed but the steps will remain in the unit.
Period:
src/assets/wise5/classroomMonitor/classroomMonitorComponents/nodeProgress/nav-item/nav-item.component.ts
- 380
+ 382
src/assets/wise5/classroomMonitor/classroomMonitorComponents/select-period/select-period.component.ts
@@ -14769,14 +14811,14 @@ The branches will be removed but the steps will remain in the unit.
Unlock for
src/assets/wise5/classroomMonitor/classroomMonitorComponents/nodeProgress/nav-item/nav-item.component.ts
- 385
+ 387
Lock for
src/assets/wise5/classroomMonitor/classroomMonitorComponents/nodeProgress/nav-item/nav-item.component.ts
- 386
+ 388