From bb05d1564c11af05b0abe86c5778321cf7a69b52 Mon Sep 17 00:00:00 2001 From: Aaron Detre Date: Sun, 22 Jun 2025 15:43:19 -0700 Subject: [PATCH 01/11] Basic timed step --- src/assets/wise5/common/ComponentContent.ts | 1 + src/assets/wise5/common/Node.ts | 1 + src/assets/wise5/services/projectService.ts | 5 + src/assets/wise5/vle/node/node.component.ts | 22 +-- .../vle/timed-node/timed-node.component.html | 85 +++++++++ .../vle/timed-node/timed-node.component.scss | 11 ++ .../timed-node/timed-node.component.spec.ts | 23 +++ .../vle/timed-node/timed-node.component.ts | 161 ++++++++++++++++++ src/assets/wise5/vle/vle.component.html | 6 +- src/assets/wise5/vle/vle.component.ts | 4 +- 10 files changed, 306 insertions(+), 13 deletions(-) create mode 100644 src/assets/wise5/vle/timed-node/timed-node.component.html create mode 100644 src/assets/wise5/vle/timed-node/timed-node.component.scss create mode 100644 src/assets/wise5/vle/timed-node/timed-node.component.spec.ts create mode 100644 src/assets/wise5/vle/timed-node/timed-node.component.ts diff --git a/src/assets/wise5/common/ComponentContent.ts b/src/assets/wise5/common/ComponentContent.ts index a9b7e3789b9..2aa9bca43dc 100644 --- a/src/assets/wise5/common/ComponentContent.ts +++ b/src/assets/wise5/common/ComponentContent.ts @@ -17,6 +17,7 @@ export interface ComponentContent { showSaveButton?: boolean; showSubmitButton?: boolean; type: string; + timeLimit?: number; } export function hasConnectedComponent( diff --git a/src/assets/wise5/common/Node.ts b/src/assets/wise5/common/Node.ts index e3b18b36933..28607ca45c0 100644 --- a/src/assets/wise5/common/Node.ts +++ b/src/assets/wise5/common/Node.ts @@ -13,6 +13,7 @@ export class Node { showSaveButton: boolean; showSubmitButton: boolean; title: string; + timed?: boolean; transitionLogic?: TransitionLogic; type: string; diff --git a/src/assets/wise5/services/projectService.ts b/src/assets/wise5/services/projectService.ts index 0fdf55ce8a9..80c960e9577 100644 --- a/src/assets/wise5/services/projectService.ts +++ b/src/assets/wise5/services/projectService.ts @@ -178,6 +178,11 @@ export class ProjectService { return node != null && node.type !== 'group'; } + isTimedNode(id: string): boolean { + const node = this.getNodeById(id); + return node != null && node.timed; + } + getGroups(): any[] { return this.groupNodes; } diff --git a/src/assets/wise5/vle/node/node.component.ts b/src/assets/wise5/vle/node/node.component.ts index cbe9bbec31a..79d47d6a022 100644 --- a/src/assets/wise5/vle/node/node.component.ts +++ b/src/assets/wise5/vle/node/node.component.ts @@ -42,8 +42,8 @@ export class NodeComponent implements OnInit { private autoSaveIntervalId: any; protected components: any[]; protected componentToVisible = {}; - protected dirtyComponentIds: any = []; - protected dirtySubmitComponentIds: any = []; + protected dirtyComponentIds: any[] = []; + protected dirtySubmitComponentIds: any[] = []; protected disabled: boolean; protected isBranchNode: boolean = false; protected isLastNode: boolean = false; @@ -74,14 +74,14 @@ export class NodeComponent implements OnInit { protected workgroupId: number; constructor( - private componentService: ComponentService, - private configService: ConfigService, - private constraintService: ConstraintService, - private nodeService: StudentNodeService, - private nodeStatusService: NodeStatusService, - private projectService: VLEProjectService, - private sessionService: SessionService, - private studentDataService: StudentDataService + protected componentService: ComponentService, + protected configService: ConfigService, + protected constraintService: ConstraintService, + protected nodeService: StudentNodeService, + protected nodeStatusService: NodeStatusService, + protected projectService: VLEProjectService, + protected sessionService: SessionService, + protected studentDataService: StudentDataService ) {} ngOnChanges(): void { @@ -204,7 +204,7 @@ export class NodeComponent implements OnInit { } } - private updateComponentVisibility(): void { + protected updateComponentVisibility(): void { this.components.forEach((component) => { const constraintResult = this.constraintService.evaluate(component.constraints); this.componentToVisible[component.id] = constraintResult.isVisible; diff --git a/src/assets/wise5/vle/timed-node/timed-node.component.html b/src/assets/wise5/vle/timed-node/timed-node.component.html new file mode 100644 index 00000000000..358cce9d174 --- /dev/null +++ b/src/assets/wise5/vle/timed-node/timed-node.component.html @@ -0,0 +1,85 @@ +@if (!stepCompleted) { +
+

{{ secondsToTimerDisplay(timerCountDown) }}

+
+ +
+
+ @if (showRubric) { +
+ +
+ } + @for (component of components; track component) { +
+ @if (componentToVisible[component.id]) { + + } +
+ } +
+ @if (node.showSaveButton) { + + } + @if (node.showSubmitButton) { + + } + @if (latestComponentState && (node.showSaveButton || node.showSubmitButton)) { + + } +
+ @if (isSurvey && !isBranchNode && !nextNodeId) { +
+ + +
+ } +
+
+} @else { + + +

You have completed this step.

+
+
+} diff --git a/src/assets/wise5/vle/timed-node/timed-node.component.scss b/src/assets/wise5/vle/timed-node/timed-node.component.scss new file mode 100644 index 00000000000..feade6e6292 --- /dev/null +++ b/src/assets/wise5/vle/timed-node/timed-node.component.scss @@ -0,0 +1,11 @@ +.mat-mdc-button { + min-width: 88px; +} + +.component { + padding: 24px 24px 0; +} + +.step-completed { + margin-top: 10%; +} \ No newline at end of file diff --git a/src/assets/wise5/vle/timed-node/timed-node.component.spec.ts b/src/assets/wise5/vle/timed-node/timed-node.component.spec.ts new file mode 100644 index 00000000000..ef8872f0c2f --- /dev/null +++ b/src/assets/wise5/vle/timed-node/timed-node.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TimedNodeComponent } from './timed-node.component'; + +describe('TimedNodeComponent', () => { + let component: TimedNodeComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TimedNodeComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(TimedNodeComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/assets/wise5/vle/timed-node/timed-node.component.ts b/src/assets/wise5/vle/timed-node/timed-node.component.ts new file mode 100644 index 00000000000..60fb59a0eac --- /dev/null +++ b/src/assets/wise5/vle/timed-node/timed-node.component.ts @@ -0,0 +1,161 @@ +import { Component } from '@angular/core'; +import { NodeComponent } from '../node/node.component'; +import { ComponentService } from '../../components/componentService'; +import { ConfigService } from '../../services/configService'; +import { ConstraintService } from '../../services/constraintService'; +import { StudentNodeService } from '../../services/studentNodeService'; +import { NodeStatusService } from '../../services/nodeStatusService'; +import { VLEProjectService } from '../vleProjectService'; +import { SessionService } from '../../services/sessionService'; +import { StudentDataService } from '../../services/studentDataService'; +import { ComponentContent } from '../../common/ComponentContent'; +import { CommonModule } from '@angular/common'; +import { ComponentComponent } from '../../components/component/component.component'; +import { ComponentStateInfoComponent } from '../../common/component-state-info/component-state-info.component'; +import { FlexLayoutModule } from '@angular/flex-layout'; +import { HelpIconComponent } from '../../themes/default/themeComponents/helpIcon/help-icon.component'; +import { MatButtonModule } from '@angular/material/button'; +import { MatDividerModule } from '@angular/material/divider'; +import { SubmitSurveyComponent } from '../submit-survey/submit-survey.component'; +import { MatCardModule } from '@angular/material/card'; + +@Component({ + selector: 'timed-node', + imports: [ + CommonModule, + ComponentComponent, + ComponentStateInfoComponent, + FlexLayoutModule, + HelpIconComponent, + MatButtonModule, + MatCardModule, + MatDividerModule, + SubmitSurveyComponent + ], + templateUrl: './timed-node.component.html', + styleUrl: './timed-node.component.scss' +}) +export class TimedNodeComponent extends NodeComponent { + private componentTimers: number[]; + private currentComponentIndex = 0; + private currentInterval: any; + protected stepCompleted = false; + protected timerCountDown: number; + + constructor( + protected componentService: ComponentService, + protected configService: ConfigService, + protected constraintService: ConstraintService, + protected nodeService: StudentNodeService, + protected nodeStatusService: NodeStatusService, + protected projectService: VLEProjectService, + protected sessionService: SessionService, + protected studentDataService: StudentDataService + ) { + super( + componentService, + configService, + constraintService, + nodeService, + nodeStatusService, + projectService, + sessionService, + studentDataService + ); + } + + async ngOnInit(): Promise { + super.ngOnInit(); + await this.skipComponentsWithWork(); + if (!this.stepCompleted) { + this.componentTimers = this.node.components.map((component: ComponentContent) => { + return component.timeLimit ?? -1; + }); + this.components.forEach((component, index) => { + this.componentToVisible[component.id] = index === this.currentComponentIndex; + }); + this.startComponentTimer(); + } + } + + private async skipComponentsWithWork() { + const studentWork = await this.studentDataService.retrieveStudentDataForSignedInStudent(); + const componentsWithWork = studentWork.componentStates.map( + (componentState) => componentState.componentId + ); + this.node.components.forEach((component, componentIndex) => { + if ( + componentsWithWork.includes(component.id) && + componentIndex >= this.currentComponentIndex + ) { + this.currentComponentIndex = componentIndex + 1; + } + }); + if (this.currentComponentIndex >= this.node.components.length) { + this.stepCompleted = true; + } + } + + private startComponentTimer(): void { + this.timerCountDown = this.componentTimers[this.currentComponentIndex]; + + this.currentInterval = setInterval(() => { + if (--this.timerCountDown === 0) { + this.componentCompleted(); + clearInterval(this.currentInterval); + } + }, 1000); + } + + private getComponentSubmitArgs(componentId: string) { + return { + nodeId: this.node.id, + componentId: componentId + }; + } + + private componentCompleted(): void { + this.saveUnsavedWork(); + this.hideComponents(++this.currentComponentIndex); + if (this.currentComponentIndex < this.node.components.length) { + this.startComponentTimer(); + } else { + this.stepCompleted = true; + } + } + + private saveUnsavedWork() { + const currentComponentId = this.node.components[this.currentComponentIndex].id; + if (this.dirtyComponentIds.includes(currentComponentId)) { + this.studentDataService.broadcastComponentSubmitTriggered( + this.getComponentSubmitArgs(currentComponentId) + ); + } + } + + private hideComponents(showComponentIndex: number): void { + this.components.forEach((component, index) => { + this.componentToVisible[component.id] = index === showComponentIndex; + }); + } + + protected secondsToTimerDisplay(secondsLeft: number): string { + const minutes = '' + Math.floor(secondsLeft / 60); + const seconds = '' + (secondsLeft % 60); + return `${this.addZeroPadding(minutes)}${minutes}:${this.addZeroPadding(seconds)}${seconds}`; + } + + private addZeroPadding(digits: string): string { + const numZeros = 2 - digits.length; + return '0'.repeat(numZeros > 0 ? numZeros : 0); + } + + protected updateComponentVisibility(): void { + return; + } + + protected proceedButtonClicked(): void { + clearInterval(this.currentInterval); + this.componentCompleted(); + } +} diff --git a/src/assets/wise5/vle/vle.component.html b/src/assets/wise5/vle/vle.component.html index cba3fda1847..24feed333a0 100644 --- a/src/assets/wise5/vle/vle.component.html +++ b/src/assets/wise5/vle/vle.component.html @@ -37,7 +37,11 @@ } @if (layoutState === 'node') { - + @if (currentNode.timed) { + + } @else { + + } } @if (layoutState === 'nav') { diff --git a/src/assets/wise5/vle/vle.component.ts b/src/assets/wise5/vle/vle.component.ts index d247d0d699b..b5b5ed7ac63 100644 --- a/src/assets/wise5/vle/vle.component.ts +++ b/src/assets/wise5/vle/vle.component.ts @@ -30,6 +30,7 @@ import { Subscription } from 'rxjs'; import { TopBarComponent } from '../../../app/student/top-bar/top-bar.component'; import { VLEProjectService } from './vleProjectService'; import { WiseLinkService } from '../../../app/services/wiseLinkService'; +import { TimedNodeComponent } from './timed-node/timed-node.component'; @Component({ imports: [ @@ -45,7 +46,8 @@ import { WiseLinkService } from '../../../app/services/wiseLinkService'; RunEndedAndLockedMessageComponent, SafeUrl, StepToolsComponent, - TopBarComponent + TopBarComponent, + TimedNodeComponent ], selector: 'vle', styleUrl: './vle.component.scss', From c0faa05f92e20a816cbb87318894a8a204a79252 Mon Sep 17 00:00:00 2001 From: Aaron Detre Date: Sun, 22 Jun 2025 20:17:59 -0700 Subject: [PATCH 02/11] Disable step tools for unfinished timed steps --- .../student-teacher-common-services.module.ts | 2 ++ src/assets/wise5/services/timedNodeService.ts | 11 +++++++ .../stepTools/step-tools.component.html | 31 ++++++++++-------- .../stepTools/step-tools.component.ts | 32 ++++++++++++++++--- .../vle/timed-node/timed-node.component.ts | 14 +++++--- src/assets/wise5/vle/vle.component.html | 2 +- src/assets/wise5/vle/vle.component.ts | 4 +++ 7 files changed, 71 insertions(+), 25 deletions(-) create mode 100644 src/assets/wise5/services/timedNodeService.ts diff --git a/src/app/student-teacher-common-services.module.ts b/src/app/student-teacher-common-services.module.ts index 98ac36eec2d..a09521dfc60 100644 --- a/src/app/student-teacher-common-services.module.ts +++ b/src/app/student-teacher-common-services.module.ts @@ -60,6 +60,7 @@ import { TeacherNodeService } from '../assets/wise5/services/teacherNodeService' import { TeacherWebSocketService } from '../assets/wise5/services/teacherWebSocketService'; import { VLEProjectService } from '../assets/wise5/vle/vleProjectService'; import { WiseLinkService } from './services/wiseLinkService'; +import { TimedNodeService } from '../assets/wise5/services/timedNodeService'; @NgModule({ providers: [ @@ -121,6 +122,7 @@ import { WiseLinkService } from './services/wiseLinkService'; TeacherDataService, TeacherNodeService, TeacherWebSocketService, + TimedNodeService, StudentProjectTranslationService, VLEProjectService, WiseLinkService diff --git a/src/assets/wise5/services/timedNodeService.ts b/src/assets/wise5/services/timedNodeService.ts new file mode 100644 index 00000000000..a3f08ddcf0b --- /dev/null +++ b/src/assets/wise5/services/timedNodeService.ts @@ -0,0 +1,11 @@ +import { Injectable } from '@angular/core'; +import { Subject } from 'rxjs'; + +@Injectable() +export class TimedNodeService { + isNodeCompletedBroadcast: Subject = new Subject(); + + broadcastIsNodeCompleted(isCompleted: boolean): void { + this.isNodeCompletedBroadcast.next(isCompleted); + } +} diff --git a/src/assets/wise5/themes/default/themeComponents/stepTools/step-tools.component.html b/src/assets/wise5/themes/default/themeComponents/stepTools/step-tools.component.html index 078ce86a3be..e8ec51aa3d4 100644 --- a/src/assets/wise5/themes/default/themeComponents/stepTools/step-tools.component.html +++ b/src/assets/wise5/themes/default/themeComponents/stepTools/step-tools.component.html @@ -3,7 +3,7 @@ -
- -
+ @if (!isUnfinishedTimedStep()) { +
+ +
+ } diff --git a/src/assets/wise5/themes/default/themeComponents/stepTools/step-tools.component.ts b/src/assets/wise5/themes/default/themeComponents/stepTools/step-tools.component.ts index 22c20b05303..7044f9a7584 100644 --- a/src/assets/wise5/themes/default/themeComponents/stepTools/step-tools.component.ts +++ b/src/assets/wise5/themes/default/themeComponents/stepTools/step-tools.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, OnInit, ViewEncapsulation } from '@angular/core'; +import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core'; import { FlexLayoutModule } from '@angular/flex-layout'; import { FormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; @@ -14,6 +14,7 @@ import { NodeStatusService } from '../../../../services/nodeStatusService'; import { ProjectService } from '../../../../services/projectService'; import { StudentDataService } from '../../../../services/studentDataService'; import { Subscription } from 'rxjs'; +import { TimedNodeService } from '../../../../services/timedNodeService'; @Component({ encapsulation: ViewEncapsulation.None, @@ -43,14 +44,17 @@ export class StepToolsComponent implements OnInit { protected nodeStatus: any; protected nodeStatuses: any; protected prevId: string; + @Input() private timedStep: boolean; + protected timedStepCompleted: boolean; private subscriptions: Subscription = new Subscription(); protected toNodeId: string; constructor( - private nodeService: NodeService, - private nodeStatusService: NodeStatusService, - private projectService: ProjectService, - private studentDataService: StudentDataService + protected nodeService: NodeService, + protected nodeStatusService: NodeStatusService, + protected projectService: ProjectService, + protected studentDataService: StudentDataService, + private timedNodeService: TimedNodeService ) {} ngOnInit(): void { @@ -76,6 +80,11 @@ export class StepToolsComponent implements OnInit { this.updateModel(); }) ); + this.subscriptions.add( + this.timedNodeService.isNodeCompletedBroadcast.subscribe( + (isStepCompleted) => (this.timedStepCompleted = isStepCompleted) + ) + ); } ngOnDestroy(): void { @@ -124,4 +133,17 @@ export class StepToolsComponent implements OnInit { protected closeNode(): void { this.nodeService.closeNode(); } + + protected isArrowDisabled(direction: 'prev' | 'next'): boolean { + const noNode = direction === 'prev' ? !this.prevId : !this.nextId; + return noNode || this.isUnfinishedTimedStep(); + } + + protected isUnfinishedTimedStep(): boolean { + if (this.timedStep) { + return !this.timedStepCompleted; + } else { + return false; + } + } } diff --git a/src/assets/wise5/vle/timed-node/timed-node.component.ts b/src/assets/wise5/vle/timed-node/timed-node.component.ts index 60fb59a0eac..4ecbe2cf653 100644 --- a/src/assets/wise5/vle/timed-node/timed-node.component.ts +++ b/src/assets/wise5/vle/timed-node/timed-node.component.ts @@ -18,9 +18,9 @@ import { MatButtonModule } from '@angular/material/button'; import { MatDividerModule } from '@angular/material/divider'; import { SubmitSurveyComponent } from '../submit-survey/submit-survey.component'; import { MatCardModule } from '@angular/material/card'; +import { TimedNodeService } from '../../services/timedNodeService'; @Component({ - selector: 'timed-node', imports: [ CommonModule, ComponentComponent, @@ -32,8 +32,9 @@ import { MatCardModule } from '@angular/material/card'; MatDividerModule, SubmitSurveyComponent ], - templateUrl: './timed-node.component.html', - styleUrl: './timed-node.component.scss' + selector: 'timed-node', + styleUrl: './timed-node.component.scss', + templateUrl: './timed-node.component.html' }) export class TimedNodeComponent extends NodeComponent { private componentTimers: number[]; @@ -50,7 +51,8 @@ export class TimedNodeComponent extends NodeComponent { protected nodeStatusService: NodeStatusService, protected projectService: VLEProjectService, protected sessionService: SessionService, - protected studentDataService: StudentDataService + protected studentDataService: StudentDataService, + private timedNodeService: TimedNodeService ) { super( componentService, @@ -67,9 +69,10 @@ export class TimedNodeComponent extends NodeComponent { async ngOnInit(): Promise { super.ngOnInit(); await this.skipComponentsWithWork(); + this.timedNodeService.broadcastIsNodeCompleted(this.stepCompleted); if (!this.stepCompleted) { this.componentTimers = this.node.components.map((component: ComponentContent) => { - return component.timeLimit ?? -1; + return component.timeLimit ?? 0; }); this.components.forEach((component, index) => { this.componentToVisible[component.id] = index === this.currentComponentIndex; @@ -121,6 +124,7 @@ export class TimedNodeComponent extends NodeComponent { this.startComponentTimer(); } else { this.stepCompleted = true; + this.timedNodeService.broadcastIsNodeCompleted(true); } } diff --git a/src/assets/wise5/vle/vle.component.html b/src/assets/wise5/vle/vle.component.html index 24feed333a0..32f8a09ad40 100644 --- a/src/assets/wise5/vle/vle.component.html +++ b/src/assets/wise5/vle/vle.component.html @@ -26,7 +26,7 @@ @if (layoutState === 'node') { - + }
Date: Sun, 22 Jun 2025 21:29:06 -0700 Subject: [PATCH 03/11] Skip previously viewed components --- .../vle/timed-node/timed-node.component.ts | 64 +++++++++++++------ 1 file changed, 43 insertions(+), 21 deletions(-) diff --git a/src/assets/wise5/vle/timed-node/timed-node.component.ts b/src/assets/wise5/vle/timed-node/timed-node.component.ts index 4ecbe2cf653..6376d059a93 100644 --- a/src/assets/wise5/vle/timed-node/timed-node.component.ts +++ b/src/assets/wise5/vle/timed-node/timed-node.component.ts @@ -1,24 +1,24 @@ +import { CommonModule } from '@angular/common'; import { Component } from '@angular/core'; -import { NodeComponent } from '../node/node.component'; +import { ComponentComponent } from '../../components/component/component.component'; +import { ComponentContent } from '../../common/ComponentContent'; import { ComponentService } from '../../components/componentService'; +import { ComponentStateInfoComponent } from '../../common/component-state-info/component-state-info.component'; import { ConfigService } from '../../services/configService'; import { ConstraintService } from '../../services/constraintService'; -import { StudentNodeService } from '../../services/studentNodeService'; -import { NodeStatusService } from '../../services/nodeStatusService'; -import { VLEProjectService } from '../vleProjectService'; -import { SessionService } from '../../services/sessionService'; -import { StudentDataService } from '../../services/studentDataService'; -import { ComponentContent } from '../../common/ComponentContent'; -import { CommonModule } from '@angular/common'; -import { ComponentComponent } from '../../components/component/component.component'; -import { ComponentStateInfoComponent } from '../../common/component-state-info/component-state-info.component'; import { FlexLayoutModule } from '@angular/flex-layout'; import { HelpIconComponent } from '../../themes/default/themeComponents/helpIcon/help-icon.component'; import { MatButtonModule } from '@angular/material/button'; +import { MatCardModule } from '@angular/material/card'; import { MatDividerModule } from '@angular/material/divider'; +import { NodeComponent } from '../node/node.component'; +import { NodeStatusService } from '../../services/nodeStatusService'; +import { SessionService } from '../../services/sessionService'; +import { StudentDataService } from '../../services/studentDataService'; +import { StudentNodeService } from '../../services/studentNodeService'; import { SubmitSurveyComponent } from '../submit-survey/submit-survey.component'; -import { MatCardModule } from '@angular/material/card'; import { TimedNodeService } from '../../services/timedNodeService'; +import { VLEProjectService } from '../vleProjectService'; @Component({ imports: [ @@ -68,7 +68,7 @@ export class TimedNodeComponent extends NodeComponent { async ngOnInit(): Promise { super.ngOnInit(); - await this.skipComponentsWithWork(); + await this.skipViewedComponents(); this.timedNodeService.broadcastIsNodeCompleted(this.stepCompleted); if (!this.stepCompleted) { this.componentTimers = this.node.components.map((component: ComponentContent) => { @@ -81,25 +81,31 @@ export class TimedNodeComponent extends NodeComponent { } } - private async skipComponentsWithWork() { + private async skipViewedComponents(): Promise { const studentWork = await this.studentDataService.retrieveStudentDataForSignedInStudent(); - const componentsWithWork = studentWork.componentStates.map( - (componentState) => componentState.componentId - ); + const componentsViewed = studentWork.events + .filter((event) => event.event === 'componentViewed') + .map((event) => event.componentId); + this.advanceCurrentComponentIndex(componentsViewed); + this.setStepCompletedIfNecessary(); + } + + private advanceCurrentComponentIndex(componentsViewed: string[]): void { this.node.components.forEach((component, componentIndex) => { - if ( - componentsWithWork.includes(component.id) && - componentIndex >= this.currentComponentIndex - ) { + if (componentsViewed.includes(component.id) && componentIndex >= this.currentComponentIndex) { this.currentComponentIndex = componentIndex + 1; } }); + } + + private setStepCompletedIfNecessary() { if (this.currentComponentIndex >= this.node.components.length) { this.stepCompleted = true; } } - private startComponentTimer(): void { + private async startComponentTimer(): Promise { + this.saveComponentViewedEvent(); this.timerCountDown = this.componentTimers[this.currentComponentIndex]; this.currentInterval = setInterval(() => { @@ -110,6 +116,22 @@ export class TimedNodeComponent extends NodeComponent { }, 1000); } + private saveComponentViewedEvent() { + this.studentDataService.saveEvent( + 'VLE', + this.node.id, + this.getCurrentComponentId(), + null, + 'Timed', + 'componentViewed', + {} + ); + } + + private getCurrentComponentId(): string { + return this.components.at(this.currentComponentIndex).id; + } + private getComponentSubmitArgs(componentId: string) { return { nodeId: this.node.id, From df166282e2ba84ca4e974ec1881a8622eb53a673 Mon Sep 17 00:00:00 2001 From: Aaron Detre Date: Mon, 23 Jun 2025 07:47:00 -0700 Subject: [PATCH 04/11] Timed step authoring --- ...-advanced-general-authoring.component.html | 5 +++++ .../node-authoring.component.html | 14 ++++++++++++ .../node-authoring.component.scss | 6 +++++ .../node-authoring.component.ts | 22 +++++++++++++------ 4 files changed, 40 insertions(+), 7 deletions(-) diff --git a/src/assets/wise5/authoringTool/node/advanced/general/node-advanced-general-authoring.component.html b/src/assets/wise5/authoringTool/node/advanced/general/node-advanced-general-authoring.component.html index 23ff423d793..772a0fc9c3d 100644 --- a/src/assets/wise5/authoringTool/node/advanced/general/node-advanced-general-authoring.component.html +++ b/src/assets/wise5/authoringTool/node/advanced/general/node-advanced-general-authoring.component.html @@ -9,4 +9,9 @@ Show Submit Button
+
+ Timed Step +
diff --git a/src/assets/wise5/authoringTool/node/node-authoring/node-authoring.component.html b/src/assets/wise5/authoringTool/node/node-authoring/node-authoring.component.html index 2fa0cdda3de..6a68ecaf999 100644 --- a/src/assets/wise5/authoringTool/node/node-authoring/node-authoring.component.html +++ b/src/assets/wise5/authoringTool/node/node-authoring/node-authoring.component.html @@ -80,6 +80,20 @@ + @if (isTimed()) { +
+ +
+ }
{ + this.saveProject().then(() => { this.deleteTranslationsService.tryDeleteComponents(components); }); } @@ -213,10 +213,18 @@ export class NodeAuthoringComponent implements OnInit { if (scroll) { this.highlightComponents([this.components[currentIndex]]); } - this.projectService.saveProject(); + this.saveProject(); } protected editComponent(componentId: string): void { this.editingComponentId = componentId; } + + protected isTimed(): boolean { + return this.node.timed !== null && this.node.timed; + } + + protected saveProject(): Promise { + return this.projectService.saveProject(); + } } From 8875e38e643c666a7bac8f7363853e1e61c06fcd Mon Sep 17 00:00:00 2001 From: Aaron Detre Date: Mon, 23 Jun 2025 07:52:35 -0700 Subject: [PATCH 05/11] Don't count below zero --- src/assets/wise5/vle/timed-node/timed-node.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/assets/wise5/vle/timed-node/timed-node.component.ts b/src/assets/wise5/vle/timed-node/timed-node.component.ts index 6376d059a93..8079f9499af 100644 --- a/src/assets/wise5/vle/timed-node/timed-node.component.ts +++ b/src/assets/wise5/vle/timed-node/timed-node.component.ts @@ -109,7 +109,7 @@ export class TimedNodeComponent extends NodeComponent { this.timerCountDown = this.componentTimers[this.currentComponentIndex]; this.currentInterval = setInterval(() => { - if (--this.timerCountDown === 0) { + if (--this.timerCountDown <= 0) { this.componentCompleted(); clearInterval(this.currentInterval); } From d6373aa12ed4e9fdeaca0eef2435a5da272c1099 Mon Sep 17 00:00:00 2001 From: Aaron Detre Date: Mon, 23 Jun 2025 08:12:19 -0700 Subject: [PATCH 06/11] Basic styling for timer and proceed button --- src/assets/wise5/vle/timed-node/timed-node.component.html | 8 +++++--- src/assets/wise5/vle/timed-node/timed-node.component.scss | 7 +++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/assets/wise5/vle/timed-node/timed-node.component.html b/src/assets/wise5/vle/timed-node/timed-node.component.html index 358cce9d174..3d8cdcf2cd5 100644 --- a/src/assets/wise5/vle/timed-node/timed-node.component.html +++ b/src/assets/wise5/vle/timed-node/timed-node.component.html @@ -1,8 +1,10 @@ @if (!stepCompleted) { -
-

{{ secondsToTimerDisplay(timerCountDown) }}

+
+

{{ secondsToTimerDisplay(timerCountDown) }}

+
-
Date: Mon, 23 Jun 2025 08:36:32 -0700 Subject: [PATCH 07/11] Fix timed step preview --- src/assets/wise5/vle/node/node.component.ts | 6 ++++- .../vle/timed-node/timed-node.component.html | 7 ++++- .../vle/timed-node/timed-node.component.ts | 26 +++++++++++-------- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/assets/wise5/vle/node/node.component.ts b/src/assets/wise5/vle/node/node.component.ts index 79d47d6a022..1817ec4bef7 100644 --- a/src/assets/wise5/vle/node/node.component.ts +++ b/src/assets/wise5/vle/node/node.component.ts @@ -191,7 +191,7 @@ export class NodeComponent implements OnInit { this.latestComponentState = latestComponentState; } - if (this.configService.isPreview()) { + if (this.isPreview()) { this.showRubric = this.node.rubric != null && this.node.rubric != ''; } @@ -475,4 +475,8 @@ export class NodeComponent implements OnInit { this.createComponentStatesResponseHandler(true) ); } + + protected isPreview(): boolean { + return this.configService.isPreview(); + } } diff --git a/src/assets/wise5/vle/timed-node/timed-node.component.html b/src/assets/wise5/vle/timed-node/timed-node.component.html index 3d8cdcf2cd5..5ab54259ccb 100644 --- a/src/assets/wise5/vle/timed-node/timed-node.component.html +++ b/src/assets/wise5/vle/timed-node/timed-node.component.html @@ -1,7 +1,12 @@ @if (!stepCompleted) {

{{ secondsToTimerDisplay(timerCountDown) }}

-
diff --git a/src/assets/wise5/vle/timed-node/timed-node.component.ts b/src/assets/wise5/vle/timed-node/timed-node.component.ts index 8079f9499af..2be11251d39 100644 --- a/src/assets/wise5/vle/timed-node/timed-node.component.ts +++ b/src/assets/wise5/vle/timed-node/timed-node.component.ts @@ -82,12 +82,14 @@ export class TimedNodeComponent extends NodeComponent { } private async skipViewedComponents(): Promise { - const studentWork = await this.studentDataService.retrieveStudentDataForSignedInStudent(); - const componentsViewed = studentWork.events - .filter((event) => event.event === 'componentViewed') - .map((event) => event.componentId); - this.advanceCurrentComponentIndex(componentsViewed); - this.setStepCompletedIfNecessary(); + if (!this.isPreview()) { + const studentWork = await this.studentDataService.retrieveStudentDataForSignedInStudent(); + const componentsViewed = studentWork.events + .filter((event) => event.event === 'componentViewed') + .map((event) => event.componentId); + this.advanceCurrentComponentIndex(componentsViewed); + this.setStepCompletedIfNecessary(); + } } private advanceCurrentComponentIndex(componentsViewed: string[]): void { @@ -151,11 +153,13 @@ export class TimedNodeComponent extends NodeComponent { } private saveUnsavedWork() { - const currentComponentId = this.node.components[this.currentComponentIndex].id; - if (this.dirtyComponentIds.includes(currentComponentId)) { - this.studentDataService.broadcastComponentSubmitTriggered( - this.getComponentSubmitArgs(currentComponentId) - ); + if (!this.isPreview()) { + const currentComponentId = this.node.components[this.currentComponentIndex].id; + if (this.dirtyComponentIds.includes(currentComponentId)) { + this.studentDataService.broadcastComponentSubmitTriggered( + this.getComponentSubmitArgs(currentComponentId) + ); + } } } From e55199139a6bcb27971a6dee7e9fd47842915a20 Mon Sep 17 00:00:00 2001 From: Aaron Detre Date: Mon, 23 Jun 2025 11:33:40 -0700 Subject: [PATCH 08/11] Fix node authoring time limit inputs only appear after reloading --- .../node/node-authoring/node-authoring.component.ts | 3 ++- src/assets/wise5/services/projectService.ts | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/assets/wise5/authoringTool/node/node-authoring/node-authoring.component.ts b/src/assets/wise5/authoringTool/node/node-authoring/node-authoring.component.ts index 7797182dcef..af59fbecc76 100644 --- a/src/assets/wise5/authoringTool/node/node-authoring/node-authoring.component.ts +++ b/src/assets/wise5/authoringTool/node/node-authoring/node-authoring.component.ts @@ -221,7 +221,8 @@ export class NodeAuthoringComponent implements OnInit { } protected isTimed(): boolean { - return this.node.timed !== null && this.node.timed; + const isTimed = this.projectService.getNodeIsTimed(this.node.id); + return isTimed !== null && isTimed; } protected saveProject(): Promise { diff --git a/src/assets/wise5/services/projectService.ts b/src/assets/wise5/services/projectService.ts index 80c960e9577..53df575867d 100644 --- a/src/assets/wise5/services/projectService.ts +++ b/src/assets/wise5/services/projectService.ts @@ -569,6 +569,11 @@ export class ProjectService { return null; } + getNodeIsTimed(nodeId: string): boolean { + const node = this.getNodeById(nodeId); + return node.timed; + } + getParentGroup(nodeId = ''): any { const node = this.getNodeById(nodeId); if (node != null) { From c246fa8b430dab044e0311e52ac06f94956986a4 Mon Sep 17 00:00:00 2001 From: Aaron Detre Date: Wed, 25 Jun 2025 13:08:54 -0700 Subject: [PATCH 09/11] Write tests --- .../node-authoring.component.ts | 4 +- src/assets/wise5/services/projectService.ts | 10 ---- .../stepTools/step-tools.component.spec.ts | 12 +++++ .../timed-node/timed-node.component.spec.ts | 54 +++++++++++++++++-- 4 files changed, 64 insertions(+), 16 deletions(-) diff --git a/src/assets/wise5/authoringTool/node/node-authoring/node-authoring.component.ts b/src/assets/wise5/authoringTool/node/node-authoring/node-authoring.component.ts index af59fbecc76..cbe1e3df52b 100644 --- a/src/assets/wise5/authoringTool/node/node-authoring/node-authoring.component.ts +++ b/src/assets/wise5/authoringTool/node/node-authoring/node-authoring.component.ts @@ -221,8 +221,8 @@ export class NodeAuthoringComponent implements OnInit { } protected isTimed(): boolean { - const isTimed = this.projectService.getNodeIsTimed(this.node.id); - return isTimed !== null && isTimed; + const node = this.projectService.getNodeById(this.node.id); + return node.timed !== null && node.timed; } protected saveProject(): Promise { diff --git a/src/assets/wise5/services/projectService.ts b/src/assets/wise5/services/projectService.ts index 53df575867d..0fdf55ce8a9 100644 --- a/src/assets/wise5/services/projectService.ts +++ b/src/assets/wise5/services/projectService.ts @@ -178,11 +178,6 @@ export class ProjectService { return node != null && node.type !== 'group'; } - isTimedNode(id: string): boolean { - const node = this.getNodeById(id); - return node != null && node.timed; - } - getGroups(): any[] { return this.groupNodes; } @@ -569,11 +564,6 @@ export class ProjectService { return null; } - getNodeIsTimed(nodeId: string): boolean { - const node = this.getNodeById(nodeId); - return node.timed; - } - getParentGroup(nodeId = ''): any { const node = this.getNodeById(nodeId); if (node != null) { diff --git a/src/assets/wise5/themes/default/themeComponents/stepTools/step-tools.component.spec.ts b/src/assets/wise5/themes/default/themeComponents/stepTools/step-tools.component.spec.ts index 3a8b9eb33da..981e8ca4944 100644 --- a/src/assets/wise5/themes/default/themeComponents/stepTools/step-tools.component.spec.ts +++ b/src/assets/wise5/themes/default/themeComponents/stepTools/step-tools.component.spec.ts @@ -54,4 +54,16 @@ describe('StepToolsComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should disable navigation on unfinished timed steps', () => { + let sections = fixture.nativeElement.querySelectorAll('.step-tools > div'); + expect(sections.length).toEqual(2); + + component['timedStep'] = true; + component['timedStepCompleted'] = false; + fixture.detectChanges(); + + sections = fixture.nativeElement.querySelectorAll('.step-tools > div'); + expect(sections.length).toEqual(1); // isUnfinishedTimedStep() returned true + }); }); diff --git a/src/assets/wise5/vle/timed-node/timed-node.component.spec.ts b/src/assets/wise5/vle/timed-node/timed-node.component.spec.ts index ef8872f0c2f..7c7e55c5355 100644 --- a/src/assets/wise5/vle/timed-node/timed-node.component.spec.ts +++ b/src/assets/wise5/vle/timed-node/timed-node.component.spec.ts @@ -1,6 +1,16 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; - import { TimedNodeComponent } from './timed-node.component'; +import { TimedNodeService } from '../../services/timedNodeService'; +import { Node } from '../../common/Node'; +import { MockProviders } from 'ng-mocks'; +import { ComponentService } from '../../components/componentService'; +import { ConfigService } from '../../services/configService'; +import { ConstraintService } from '../../services/constraintService'; +import { SessionService } from '../../services/sessionService'; +import { StudentDataService } from '../../services/studentDataService'; +import { NodeStatusService } from '../../services/nodeStatusService'; +import { StudentNodeService } from '../../services/studentNodeService'; +import { VLEProjectService } from '../vleProjectService'; describe('TimedNodeComponent', () => { let component: TimedNodeComponent; @@ -8,16 +18,52 @@ describe('TimedNodeComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [TimedNodeComponent] - }) - .compileComponents(); + imports: [TimedNodeComponent], + providers: [ + MockProviders( + ComponentService, + ConfigService, + ConstraintService, + StudentNodeService, + NodeStatusService, + VLEProjectService, + SessionService, + StudentDataService, + TimedNodeService + ) + ] + }).compileComponents(); + spyOn(TestBed.inject(TimedNodeService), 'broadcastIsNodeCompleted'); + spyOn(TestBed.inject(ConfigService), 'isPreview').and.returnValue(false); + spyOn( + TestBed.inject(StudentDataService), + 'retrieveStudentDataForSignedInStudent' + ).and.returnValue( + new Promise(() => { + events: []; + }) + ); fixture = TestBed.createComponent(TimedNodeComponent); component = fixture.componentInstance; + component.node = new Node(); + component.node.components = [{ id: 'c1', type: 'DG', timeLimit: 5 }]; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should show timer and proceed button if unfinished', () => { + expect(fixture.nativeElement.querySelector('.timed-node-tools')).toBeTruthy(); + expect(fixture.nativeElement.querySelector('.step-completed')).toBeNull(); + }); + + it('should show step completed message if finished', () => { + component['stepCompleted'] = true; + fixture.detectChanges(); + expect(fixture.nativeElement.querySelector('.timed-node-tools')).toBeNull(); + expect(fixture.nativeElement.querySelector('.step-completed')).toBeTruthy(); + }); }); From 62db0d3eb1ae8f13fefcb9f9d9e4f9a406f4d999 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 25 Jun 2025 20:16:45 +0000 Subject: [PATCH 10/11] Updated messages --- src/messages.xlf | 50 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/src/messages.xlf b/src/messages.xlf index c30a4649573..2a6b4d228eb 100644 --- a/src/messages.xlf +++ b/src/messages.xlf @@ -1232,7 +1232,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/assets/wise5/authoringTool/node/node-authoring/node-authoring.component.html - 98,100 + 112,114 src/assets/wise5/components/common/feedbackRule/edit-feedback-rules/edit-feedback-rules.component.html @@ -1263,7 +1263,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/assets/wise5/authoringTool/node/node-authoring/node-authoring.component.html - 109,112 + 123,126 src/assets/wise5/components/common/feedbackRule/edit-feedback-rules/edit-feedback-rules.component.html @@ -1667,6 +1667,10 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.src/assets/wise5/vle/node/node.component.html 56,60 + + src/assets/wise5/vle/timed-node/timed-node.component.html + 68,71 + Run ID @@ -6882,6 +6886,10 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.src/assets/wise5/vle/node/node.component.html 46,50 + + src/assets/wise5/vle/timed-node/timed-node.component.html + 58,61 + Team hasn't worked on yet. @@ -9000,6 +9008,10 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.src/assets/wise5/vle/node/node.component.html 43,47 + + src/assets/wise5/vle/timed-node/timed-node.component.html + 55,58 + Tag created @@ -10503,7 +10515,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/assets/wise5/vle/vle.component.ts - 211 + 213 @@ -10518,7 +10530,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/assets/wise5/vle/vle.component.ts - 212 + 214 @@ -11361,6 +11373,10 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.src/assets/wise5/vle/node/node.component.html 59,62 + + src/assets/wise5/vle/timed-node/timed-node.component.html + 71,74 + Creating Branch @@ -12189,6 +12205,13 @@ The branches will be removed but the steps will remain in the unit. 9,13 + + Timed Step + + src/assets/wise5/authoringTool/node/advanced/general/node-advanced-general-authoring.component.html + 14,18 + + Edit Step JSON @@ -12482,6 +12505,13 @@ The branches will be removed but the steps will remain in the unit. 74,76 + + Time Limit (seconds) + + src/assets/wise5/authoringTool/node/node-authoring/node-authoring.component.html + 86,89 + + Are you sure you want to delete this component? @@ -14136,6 +14166,10 @@ The branches will be removed but the steps will remain in the unit. src/assets/wise5/vle/node/node.component.html 9,12 + + src/assets/wise5/vle/timed-node/timed-node.component.html + 21,24 + Class Report @@ -15887,7 +15921,7 @@ Are you sure you want to proceed? src/assets/wise5/themes/default/themeComponents/stepTools/step-tools.component.html - 60,63 + 61,64 @@ -22246,11 +22280,11 @@ If this problem continues, let your teacher know and move on to the next activit src/assets/wise5/themes/default/themeComponents/stepTools/step-tools.component.html - 70,73 + 72,75 src/assets/wise5/themes/default/themeComponents/stepTools/step-tools.component.html - 72,75 + 74,77 @@ -22342,7 +22376,7 @@ If this problem continues, let your teacher know and move on to the next activit Next Item src/assets/wise5/themes/default/themeComponents/stepTools/step-tools.component.html - 58,61 + 59,62 From ad06ca4b5066947ea9166f62191ee49a72cbcf70 Mon Sep 17 00:00:00 2001 From: Aaron Detre Date: Wed, 25 Jun 2025 18:00:35 -0700 Subject: [PATCH 11/11] Minor code improvement --- .../themeComponents/stepTools/step-tools.component.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/assets/wise5/themes/default/themeComponents/stepTools/step-tools.component.ts b/src/assets/wise5/themes/default/themeComponents/stepTools/step-tools.component.ts index 7044f9a7584..01db5990399 100644 --- a/src/assets/wise5/themes/default/themeComponents/stepTools/step-tools.component.ts +++ b/src/assets/wise5/themes/default/themeComponents/stepTools/step-tools.component.ts @@ -50,10 +50,10 @@ export class StepToolsComponent implements OnInit { protected toNodeId: string; constructor( - protected nodeService: NodeService, - protected nodeStatusService: NodeStatusService, - protected projectService: ProjectService, - protected studentDataService: StudentDataService, + private nodeService: NodeService, + private nodeStatusService: NodeStatusService, + private projectService: ProjectService, + private studentDataService: StudentDataService, private timedNodeService: TimedNodeService ) {}