From b9c561e4b8081335e171841535dfc306151bb3c7 Mon Sep 17 00:00:00 2001 From: Geoffrey Kwan Date: Thu, 6 Apr 2023 18:25:45 -0400 Subject: [PATCH 01/43] feat(Run): Allow teachers to archive runs #1012 --- src/app/domain/project.ts | 1 + .../teacher/run-menu/run-menu.component.html | 10 + .../run-menu/run-menu.component.spec.ts | 96 +++--- .../teacher/run-menu/run-menu.component.ts | 43 ++- .../teacher-run-list-item.component.html | 15 +- .../teacher-run-list-item.component.scss | 4 + .../teacher-run-list-item.component.spec.ts | 40 ++- .../teacher-run-list-item.component.ts | 14 +- .../teacher-run-list.component.html | 86 ++++-- .../teacher-run-list.component.scss | 19 ++ .../teacher-run-list.component.spec.ts | 276 ++++++++++++++++-- .../teacher-run-list.component.ts | 204 ++++++++++--- src/app/teacher/teacher-run.ts | 1 + src/app/teacher/teacher.module.ts | 2 + src/app/teacher/teacher.service.ts | 26 ++ src/messages.xlf | 239 +++++++++++---- 16 files changed, 866 insertions(+), 210 deletions(-) diff --git a/src/app/domain/project.ts b/src/app/domain/project.ts index 6558c9ad945..47f76d86b06 100644 --- a/src/app/domain/project.ts +++ b/src/app/domain/project.ts @@ -18,6 +18,7 @@ export class Project { wiseVersion: number; uri: String; license: String; + isDeleted: boolean; static readonly VIEW_PERMISSION: number = 1; static readonly EDIT_PERMISSION: number = 2; diff --git a/src/app/teacher/run-menu/run-menu.component.html b/src/app/teacher/run-menu/run-menu.component.html index 95c17ba96e4..599f4ee8852 100644 --- a/src/app/teacher/run-menu/run-menu.component.html +++ b/src/app/teacher/run-menu/run-menu.component.html @@ -29,5 +29,15 @@ report_problem Report Problem + + + folder + Archive + + + folder_off + Unarchive + + diff --git a/src/app/teacher/run-menu/run-menu.component.spec.ts b/src/app/teacher/run-menu/run-menu.component.spec.ts index 388c47d57c2..7520900970d 100644 --- a/src/app/teacher/run-menu/run-menu.component.spec.ts +++ b/src/app/teacher/run-menu/run-menu.component.spec.ts @@ -1,8 +1,7 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { RunMenuComponent } from './run-menu.component'; import { TeacherService } from '../teacher.service'; -import { Project } from '../../domain/project'; -import { BehaviorSubject, Observable } from 'rxjs'; +import { BehaviorSubject, Observable, of, throwError } from 'rxjs'; import { MatDialog } from '@angular/material/dialog'; import { MatMenuModule } from '@angular/material/menu'; import { ConfigService } from '../../services/config.service'; @@ -12,6 +11,8 @@ import { TeacherRun } from '../teacher-run'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { Course } from '../../domain/course'; import { RouterTestingModule } from '@angular/router/testing'; +import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; export class MockTeacherService { checkClassroomAuthorization(): Observable { @@ -26,6 +27,12 @@ export class MockTeacherService { observer.complete(); }); } + archiveRun(run: TeacherRun) { + return of({}); + } + unarchiveRun(run: TeacherRun) { + return of({}); + } } export class MockUserService { @@ -56,44 +63,67 @@ export class MockConfigService { } } -describe('RunMenuComponent', () => { - let component: RunMenuComponent; - let fixture: ComponentFixture; +let component: RunMenuComponent; +let fixture: ComponentFixture; +let snackBarSpy: jasmine.Spy; +let teacherService: TeacherService; - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - imports: [MatMenuModule, RouterTestingModule], - declarations: [RunMenuComponent], - providers: [ - { provide: TeacherService, useClass: MockTeacherService }, - { provide: UserService, useClass: MockUserService }, - { provide: ConfigService, useClass: MockConfigService }, - { provide: MatDialog, useValue: {} } - ], - schemas: [NO_ERRORS_SCHEMA] - }).compileComponents(); - })); +describe('RunMenuComponent', () => { + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [BrowserAnimationsModule, MatMenuModule, MatSnackBarModule, RouterTestingModule], + declarations: [RunMenuComponent], + providers: [ + { provide: TeacherService, useClass: MockTeacherService }, + { provide: UserService, useClass: MockUserService }, + { provide: ConfigService, useClass: MockConfigService }, + { provide: MatDialog, useValue: {} } + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + }) + ); beforeEach(() => { fixture = TestBed.createComponent(RunMenuComponent); component = fixture.componentInstance; - const run: TeacherRun = new TeacherRun(); - run.id = 1; - run.name = 'Photosynthesis'; const owner = new User(); - owner.id = 1; - run.owner = owner; - const project = new Project(); - project.id = 1; - project.owner = owner; - project.sharedOwners = []; - run.project = project; - run.sharedOwners = []; - component.run = run; + component.run = new TeacherRun({ + id: 1, + name: 'Photosynthesis', + owner: owner, + project: { + id: 1, + owner: owner, + sharedOwners: [] + } + }); + teacherService = TestBed.inject(TeacherService); + snackBarSpy = spyOn(TestBed.inject(MatSnackBar), 'open'); fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); - }); + archive(); + unarchive(); }); + +function archive() { + describe('archive()', () => { + it('should archive a run', () => { + component.archive(); + expect(component.run.project.isDeleted).toEqual(true); + expect(snackBarSpy).toHaveBeenCalledWith('Successfully Archived Run'); + }); + }); +} + +function unarchive() { + describe('unarchive()', () => { + it('should unarchive a run', () => { + component.unarchive(); + expect(component.run.project.isDeleted).toEqual(false); + expect(snackBarSpy).toHaveBeenCalledWith('Successfully Unarchived Run'); + }); + }); +} diff --git a/src/app/teacher/run-menu/run-menu.component.ts b/src/app/teacher/run-menu/run-menu.component.ts index be4698a3928..5e9f39e45ff 100644 --- a/src/app/teacher/run-menu/run-menu.component.ts +++ b/src/app/teacher/run-menu/run-menu.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { ShareRunDialogComponent } from '../share-run-dialog/share-run-dialog.component'; import { LibraryProjectDetailsComponent } from '../../modules/library/library-project-details/library-project-details.component'; @@ -8,6 +8,8 @@ import { ConfigService } from '../../services/config.service'; import { RunSettingsDialogComponent } from '../run-settings-dialog/run-settings-dialog.component'; import { EditRunWarningDialogComponent } from '../edit-run-warning-dialog/edit-run-warning-dialog.component'; import { Router } from '@angular/router'; +import { TeacherService } from '../teacher.service'; +import { MatSnackBar } from '@angular/material/snack-bar'; @Component({ selector: 'app-run-menu', @@ -16,21 +18,26 @@ import { Router } from '@angular/router'; }) export class RunMenuComponent implements OnInit { @Input() run: TeacherRun; + @Output() runArchiveStatusChangedEvent: EventEmitter = new EventEmitter(); editLink: string = ''; + isOwner: boolean; reportProblemLink: string = ''; constructor( private dialog: MatDialog, private userService: UserService, private configService: ConfigService, - private router: Router + private router: Router, + private snackBar: MatSnackBar, + private teacherService: TeacherService ) {} ngOnInit() { this.editLink = `${this.configService.getContextPath()}/teacher/edit/unit/${ this.run.project.id }`; + this.isOwner = this.run.isOwner(this.userService.getUserId()); this.reportProblemLink = `${this.configService.getContextPath()}/contact?runId=${this.run.id}`; } @@ -57,10 +64,6 @@ export class RunMenuComponent implements OnInit { return this.run.canGradeAndManage(this.userService.getUserId()); } - isOwner() { - return this.run.isOwner(this.userService.getUserId()); - } - isRunCompleted() { return this.run.isCompleted(this.configService.getCurrentServerTime()); } @@ -86,4 +89,32 @@ export class RunMenuComponent implements OnInit { this.router.navigateByUrl(this.editLink); } } + + archive(): void { + const run = this.run; + this.teacherService.archiveRun(run).subscribe({ + next: () => { + run.project.isDeleted = true; + this.runArchiveStatusChangedEvent.emit(run); + this.snackBar.open($localize`Successfully Archived Run`); + }, + error: () => { + this.snackBar.open($localize`Error Archiving Run`); + } + }); + } + + unarchive(): void { + const run = this.run; + this.teacherService.unarchiveRun(run).subscribe({ + next: () => { + run.project.isDeleted = false; + this.runArchiveStatusChangedEvent.emit(run); + this.snackBar.open($localize`Successfully Unarchived Run`); + }, + error: () => { + this.snackBar.open($localize`Error Unarchiving Run`); + } + }); + } } diff --git a/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html b/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html index db1e2b3d2bb..934e2ee95ec 100644 --- a/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html +++ b/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html @@ -52,10 +52,16 @@ +
- +
@@ -98,7 +107,7 @@ fxLayoutGap="16px" > Last student login: {{ run.lastRun | date : 'short' }}Last student login: {{ run.lastRun | date: 'short' }} + + - {{ run.startTime | date : 'mediumDate' }} + {{ run.startTime | date: 'mediumDate' }} - - {{ run.endTime | date : 'mediumDate' }} + - {{ run.endTime | date: 'mediumDate' }} - + diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.scss b/src/app/teacher/teacher-run-list/teacher-run-list.component.scss index d893cc68870..185534a1e9e 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.scss +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.scss @@ -23,3 +23,22 @@ h2 { .notice { font-weight: 500; } + +.select-all-controls { + margin-left: 38px; + height: 36px; +} + +.select-all-check-box { + margin-right: 6px; +} + +.select-all-drop-down { + margin-right: 24px; +} + +.num-selected-runs-display { + font-weight: 500; + font-size: 14px; + margin-right: 24px; +} \ No newline at end of file diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts b/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts index 0c24871e9fb..da677195e66 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts @@ -1,5 +1,5 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { defer, Observable } from 'rxjs'; +import { defer, Observable, of } from 'rxjs'; import { TeacherRunListComponent } from './teacher-run-list.component'; import { TeacherService } from '../teacher.service'; import { Project } from '../../domain/project'; @@ -8,6 +8,9 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ConfigService } from '../../services/config.service'; import { RouterTestingModule } from '@angular/router/testing'; import { UserService } from '../../services/user.service'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; +import { User } from '../../domain/user'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; class TeacherScheduleStubComponent {} @@ -16,6 +19,12 @@ export function fakeAsyncResponse(data: T) { } export class MockTeacherService { + archiveRuns(): Observable { + return of([]); + } + unarchiveRuns(): Observable { + return of([]); + } getRuns(): Observable { const runs: TeacherRun[] = []; const run1 = new TeacherRun(); @@ -66,18 +75,35 @@ export class MockUserService { } } -describe('TeacherRunListComponent', () => { - let component: TeacherRunListComponent; - let fixture: ComponentFixture; +let component: TeacherRunListComponent; +let configService: ConfigService; +const currentTime = new Date().getTime(); +const dummyClickEvent: any = { preventDefault: () => {} }; +let fixture: ComponentFixture; +let run1: TeacherRun; +let run2: TeacherRun; +let run3: TeacherRun; +let teacherService: TeacherService; +function createRun(id: number, ownerId: number): TeacherRun { + return new TeacherRun({ + id: id, + project: { id: id, isDeleted: false }, + owner: new User({ id: ownerId }) + }); +} + +describe('TeacherRunListComponent', () => { beforeEach( waitForAsync(() => { TestBed.configureTestingModule({ declarations: [TeacherRunListComponent], imports: [ + BrowserAnimationsModule, RouterTestingModule.withRoutes([ { path: 'teacher/home/schedule', component: TeacherScheduleStubComponent } - ]) + ]), + MatSnackBarModule ], providers: [ { provide: TeacherService, useClass: MockTeacherService }, @@ -91,40 +117,226 @@ describe('TeacherRunListComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(TeacherRunListComponent); + configService = TestBed.inject(ConfigService); + teacherService = TestBed.inject(TeacherService); component = fixture.componentInstance; + spyOn(teacherService, 'getRuns').and.returnValue( + of([createRun(1, 1), createRun(2, 1), createRun(3, 1)]) + ); + spyOn(configService, 'getCurrentServerTime').and.returnValue(currentTime); fixture.detectChanges(); + run1 = component.runs[0]; + run2 = component.runs[1]; + run3 = component.runs[2]; + }); + + archiveSelectedRuns(); + isShowArchiveChanged(); + runArchiveStatusChanged(); + runSelectedStatusChanged(); + selectAllRunsCheckboxClicked(); + selectRunsOptionChosen(); + sortByStartTimeDesc(); + unarchiveSelectedRuns(); +}); + +function sortByStartTimeDesc() { + describe('sortByStartTimeDesc()', () => { + it('should sort runs by start date', () => { + const run3 = new TeacherRun(); + run3.id = 3; + run3.name = 'Planet Earth'; + run3.numStudents = 10; + run3.periods = ['6', '7']; + run3.startTime = new Date('2018-02-02T00:00:00.0').getTime(); + const project3 = new Project(); + project3.id = 1; + project3.name = 'Planet Earth'; + project3.projectThumb = ''; + run3.project = project3; + component.runs.push(run3); + component.runs.sort(component.sortByStartTimeDesc); + expect(isRunsSortedByStartTimeDesc(component.runs)).toBeTruthy(); + }); }); +} - function isRunsSortedByStartTimeDesc(runs: TeacherRun[]): boolean { - let previous: number = null; - for (let run of runs) { - let current = run.startTime; - if (previous && previous < current) { - return false; - } - previous = current; +function isRunsSortedByStartTimeDesc(runs: TeacherRun[]): boolean { + let previous: number = null; + for (let run of runs) { + let current = run.startTime; + if (previous && previous < current) { + return false; } - return true; + previous = current; } + return true; +} - it('should create', () => { - expect(component).toBeTruthy(); +function setRunsIsSelected(runs: TeacherRun[], isSelected: boolean[]): void { + runs.forEach((run: TeacherRun, index: number) => { + run.isSelected = isSelected[index]; }); +} - it('should sort runs by start date', () => { - const run3 = new TeacherRun(); - run3.id = 3; - run3.name = 'Planet Earth'; - run3.numStudents = 10; - run3.periods = ['6', '7']; - run3.startTime = new Date('2018-02-02T00:00:00.0').getTime(); - const project3 = new Project(); - project3.id = 1; - project3.name = 'Planet Earth'; - project3.projectThumb = ''; - run3.project = project3; - component.runs.push(run3); - component.runs.sort(component.sortByStartTimeDesc); - expect(isRunsSortedByStartTimeDesc(component.runs)).toBeTruthy(); +function expectRunsIsArchived(runs: TeacherRun[], isArchived: boolean[]): void { + runs.forEach((run: TeacherRun, index: number) => { + expect(run.project.isDeleted).toEqual(isArchived[index]); }); -}); +} + +function archiveSelectedRuns(): void { + describe('archiveSelectedRuns()', () => { + it('should archive selected runs', () => { + setRunsIsSelected([run1, run2, run3], [true, true, false]); + spyOn(teacherService, 'archiveRuns').and.returnValue(of([run1, run2])); + component.archiveSelectedRuns(); + expectRunsIsSelected([run1, run2, run3], [false, false, false]); + expectRunsIsArchived([run1, run2, run3], [true, true, false]); + }); + }); +} + +function unarchiveSelectedRuns(): void { + describe('unarchiveSelectedRuns()', () => { + it('should unarchive selected runs', () => { + setRunsIsSelected([run1, run2, run3], [true, true, false]); + spyOn(teacherService, 'unarchiveRuns').and.returnValue(of([run1, run2])); + component.unarchiveSelectedRuns(); + expectRunsIsSelected([run1, run2, run3], [false, false, false]); + expectRunsIsArchived([run1, run2, run3], [false, false, false]); + }); + }); +} + +function isShowArchiveChanged(): void { + describe('isShowArchiveChanged()', () => { + describe('active runs are shown and some runs are selected', () => { + it('should unselect the runs', () => { + setRunsIsSelected([run1, run2, run3], [true, false, true]); + setAllRunsCheckbox(false, true); + component.isShowArchivedChanged(); + expectRunsIsSelected([run1, run2, run3], [false, false, false]); + expectSelectAllRunsCheckbox(false, false); + }); + }); + }); +} + +function selectAllRunsCheckboxClicked(): void { + describe('selectAllRunsCheckboxClicked()', () => { + describe('select all runs checkbox is not checked', () => { + it('when select all runs checkbox is clicked it should select all runs', () => { + setAllRunsCheckbox(false, false); + component.selectAllRunsCheckboxClicked(dummyClickEvent); + expectSelectAllRunsCheckbox(true, false); + }); + }); + describe('select all runs checkbox is checked', () => { + it('when select all runs checkbox is clicked it should unselect all runs', () => { + setAllRunsCheckbox(true, false); + component.selectAllRunsCheckboxClicked(dummyClickEvent); + expectSelectAllRunsCheckbox(false, false); + }); + }); + describe('select all runs checkbox is indeterminate checked', () => { + it('when select all runs checkbox is clicked it should unselect all runs', () => { + setAllRunsCheckbox(false, true); + component.selectAllRunsCheckboxClicked(dummyClickEvent); + expectSelectAllRunsCheckbox(false, false); + }); + }); + }); +} + +function setAllRunsCheckbox(isSelectedAllRuns: boolean, isSelectedSomeRuns: boolean): void { + component.isSelectedAllRuns = isSelectedAllRuns; + component.isSelectedSomeRuns = isSelectedSomeRuns; +} + +function expectSelectAllRunsCheckbox( + isSelectedAllRuns: boolean, + isSelectedSomeRuns: boolean +): void { + expect(component.isSelectedAllRuns).toEqual(isSelectedAllRuns); + expect(component.isSelectedSomeRuns).toEqual(isSelectedSomeRuns); +} + +function runSelectedStatusChanged(): void { + describe('runSelectedStatusChanged()', () => { + describe('one run is selected', () => { + it('should show 1 run selected and indeterminate for the select all checkbox', () => { + setRunsIsSelected([run1, run2, run3], [true, false, false]); + component.runSelectedStatusChanged(); + expect(component.numSelectedRuns).toEqual(1); + expectSelectAllRunsCheckbox(false, true); + }); + }); + describe('two runs are selected', () => { + it('should show 2 runs selected and indeterminate for the select all checkbox', () => { + setRunsIsSelected([run1, run2, run3], [true, true, false]); + component.runSelectedStatusChanged(); + expect(component.numSelectedRuns).toEqual(2); + expectSelectAllRunsCheckbox(false, true); + }); + }); + describe('all runs are selected', () => { + it('should show 3 runs selected and checked for the select all checkbox', () => { + setRunsIsSelected([run1, run2, run3], [true, true, true]); + component.runSelectedStatusChanged(); + expect(component.numSelectedRuns).toEqual(3); + expectSelectAllRunsCheckbox(true, false); + }); + }); + }); +} + +function selectRunsOptionChosen(): void { + describe('selectRunsOptionChosen()', () => { + it('when all is chosen, it should select all runs', () => { + component.selectRunsOptionChosen('all'); + expectRunsIsSelected(component.filteredRuns, [true, true, true]); + }); + it('when none is chosen, it should select no runs', () => { + component.selectRunsOptionChosen('none'); + expectRunsIsSelected(component.filteredRuns, [false, false, false]); + }); + it('when running is chosen, it should select running runs', () => { + setRunIsCompleted(run2); + component.selectRunsOptionChosen('running'); + expectRunsIsSelected(component.filteredRuns, [true, false, true]); + }); + it('when completed is chosen, it should select completed runs', () => { + setRunIsCompleted(run2); + component.selectRunsOptionChosen('completed'); + expectRunsIsSelected(component.filteredRuns, [false, true, false]); + }); + }); +} + +function setRunIsCompleted(run: TeacherRun): void { + run.endTime = currentTime - 1000; +} + +function expectRunsIsSelected(runs: TeacherRun[], expectRunsIsSelected: boolean[]): void { + runs.forEach((run: TeacherRun, index: number) => { + expect(run.isSelected).toEqual(expectRunsIsSelected[index]); + }); +} + +function runArchiveStatusChanged(): void { + describe('runArchiveStatusChanged()', () => { + it('when a run is archived, it should no longer be displayed in the active view', () => { + expect(!component.isShowArchived); + expect(component.filteredRuns.length).toEqual(3); + setRunIsArchived(run1, true); + component.runArchiveStatusChanged(); + expect(!component.isShowArchived); + expect(component.filteredRuns.length).toEqual(2); + }); + }); +} + +function setRunIsArchived(run: TeacherRun, isArchived: boolean): void { + run.project.isDeleted = isArchived; +} diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.ts b/src/app/teacher/teacher-run-list/teacher-run-list.component.ts index 30ffae67d3d..bad1264fc39 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.ts +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.ts @@ -7,6 +7,7 @@ import { formatDate } from '@angular/common'; import { Observable, of, Subscription } from 'rxjs'; import { UserService } from '../../services/user.service'; import { mergeMap } from 'rxjs/operators'; +import { MatSnackBar } from '@angular/material/snack-bar'; @Component({ selector: 'app-teacher-run-list', @@ -20,9 +21,12 @@ export class TeacherRunListComponent implements OnInit { filteredRuns: TeacherRun[] = []; loaded: boolean = false; searchValue: string = ''; - periods: string[] = []; filterOptions: any[]; filterValue: string = ''; + isSelectedAllRuns: boolean = false; + isSelectedSomeRuns: boolean = false; + isShowArchived: boolean = false; + numSelectedRuns: number = 0; showAll: boolean = false; subscriptions: Subscription = new Subscription(); @@ -31,6 +35,7 @@ export class TeacherRunListComponent implements OnInit { @Inject(LOCALE_ID) private localeID: string, private route: ActivatedRoute, private router: Router, + private snackBar: MatSnackBar, private teacherService: TeacherService, private userService: UserService ) {} @@ -92,9 +97,6 @@ export class TeacherRunListComponent implements OnInit { private processRuns(): void { this.filteredRuns = this.runs; - this.populatePeriods(); - this.periods.sort(); - this.populateFilterOptions(); this.performSearchAndFilter(); } @@ -102,60 +104,39 @@ export class TeacherRunListComponent implements OnInit { return b.startTime - a.startTime; } - private populatePeriods(): void { - this.periods = []; - for (const run of this.runs) { - for (const period of run.periods) { - if (!this.periods.includes(period)) { - this.periods.push(period); - } - } - } - } - - private populateFilterOptions(): void { - this.filterOptions = [{ value: '', label: $localize`All Periods` }]; - for (const period of this.periods) { - this.filterOptions.push({ value: period, label: period }); - } - } - - runSpansDays(run: TeacherRun) { + runSpansDays(run: TeacherRun): boolean { const startDay = formatDate(run.startTime, 'shortDate', this.localeID); const endDay = formatDate(run.endTime, 'shortDate', this.localeID); return startDay != endDay; } activeTotal(): number { - let total = 0; const now = this.configService.getCurrentServerTime(); - for (const run of this.filteredRuns) { - if (run.isActive(now)) { - total++; - } - } - return total; + return this.filteredRuns.filter((run: TeacherRun) => run.isActive(now)).length; + } + + completedTotal(): number { + const now = this.configService.getCurrentServerTime(); + return this.filteredRuns.filter( + (run: TeacherRun) => !run.isActive(now) && !run.isScheduled(now) + ).length; } scheduledTotal(): number { - let total = 0; const now = this.configService.getCurrentServerTime(); - for (const run of this.filteredRuns) { - if (run.isScheduled(now)) { - total++; - } - } - return total; + return this.filteredRuns.filter((run: TeacherRun) => run.isScheduled(now)).length; } private performSearchAndFilter(): void { this.filteredRuns = this.searchValue ? this.performSearch(this.searchValue) : this.runs; - this.performFilter(this.filterValue); + this.performFilter(); + this.updateNumSelectedRuns(); } searchChanged(searchValue: string): void { this.searchValue = searchValue; this.performSearchAndFilter(); + this.turnOnShowAll(); } filterChanged(value: string): void { @@ -163,15 +144,17 @@ export class TeacherRunListComponent implements OnInit { this.performSearchAndFilter(); } - private performFilter(value: string): void { + private performFilter(): void { this.filteredRuns = this.filteredRuns.filter((run: TeacherRun) => { - return value === '' || run.periods.includes(value); + return ( + (!this.isShowArchived && !run.project.isDeleted) || + (this.isShowArchived && run.project.isDeleted) + ); }); } - private performSearch(searchValue: string) { + private performSearch(searchValue: string): TeacherRun[] { searchValue = searchValue.toLocaleLowerCase(); - // TODO: extract this for global use? return this.runs.filter((run: TeacherRun) => Object.keys(run).some((prop) => { const value = run[prop]; @@ -192,7 +175,7 @@ export class TeacherRunListComponent implements OnInit { this.performSearchAndFilter(); } - isRunActive(run) { + isRunActive(run: TeacherRun): boolean { return run.isActive(this.configService.getCurrentServerTime()); } @@ -216,4 +199,139 @@ export class TeacherRunListComponent implements OnInit { } } } + + isShowArchivedChanged(): void { + this.turnOnShowAll(); + this.unselectAllRuns(); + this.updateSelectAllCheckboxAndNumRunsSelected(); + this.performSearchAndFilter(); + } + + selectAllRunsCheckboxClicked(event: any): void { + this.turnOnShowAll(); + if (this.isSelectedAllRuns || this.isSelectedSomeRuns) { + this.unselectAllRuns(); + } else { + this.selectAllFilteredRuns(); + } + this.updateSelectAllCheckboxAndNumRunsSelected(); + event.preventDefault(); + } + + private unselectAllRuns(): void { + this.isSelectedAllRuns = false; + this.isSelectedSomeRuns = false; + for (const run of this.runs) { + run.isSelected = false; + } + } + + private selectAllFilteredRuns(): void { + this.filteredRuns + .filter((run: TeacherRun) => !run.shared) + .forEach((run: TeacherRun) => { + run.isSelected = true; + }); + } + + selectRunsOptionChosen(value: string): void { + this.turnOnShowAll(); + this.isSelectedAllRuns = value === 'all'; + this.isSelectedAllRuns = value === 'none'; + this.filteredRuns + .filter((run: TeacherRun) => !run.shared) + .forEach((run: TeacherRun) => { + switch (value) { + case 'all': + run.isSelected = true; + break; + case 'none': + run.isSelected = false; + break; + case 'running': + run.isSelected = !run.isCompleted(this.configService.getCurrentServerTime()); + break; + case 'completed': + run.isSelected = run.isCompleted(this.configService.getCurrentServerTime()); + break; + } + }); + this.updateSelectAllCheckboxAndNumRunsSelected(); + } + + private updateSelectAllCheckboxAndNumRunsSelected(): void { + this.updateSelectAllCheckbox(); + this.updateNumSelectedRuns(); + } + + private updateSelectAllCheckbox(): void { + const numFilteredRuns = this.filteredRuns.length; + const numSelectedRuns = this.getNumSelectedRuns(); + this.isSelectedAllRuns = numSelectedRuns > 0 && numSelectedRuns === numFilteredRuns; + this.isSelectedSomeRuns = numSelectedRuns > 0 && numSelectedRuns !== numFilteredRuns; + } + + private updateNumSelectedRuns(): void { + this.numSelectedRuns = this.getNumSelectedRuns(); + } + + private getNumSelectedRuns(): number { + return this.getSelectedRuns().length; + } + + archiveSelectedRuns(): Subscription { + const runs = this.getSelectedRuns(); + return this.teacherService.archiveRuns(runs).subscribe({ + next: () => { + this.setRunsIsDeleted(runs, true); + this.unselectAllRuns(); + this.updateSelectAllCheckboxAndNumRunsSelected(); + this.performSearchAndFilter(); + this.snackBar.open($localize`Successfully Archived ${runs.length} Runs`); + }, + error: () => { + this.snackBar.open($localize`Error Archiving Runs`); + } + }); + } + + unarchiveSelectedRuns(): Subscription { + const runs = this.getSelectedRuns(); + return this.teacherService.unarchiveRuns(runs).subscribe({ + next: () => { + this.setRunsIsDeleted(runs, false); + this.unselectAllRuns(); + this.updateSelectAllCheckboxAndNumRunsSelected(); + this.performSearchAndFilter(); + this.snackBar.open($localize`Successfully Unarchived ${runs.length} Runs`); + }, + error: () => { + this.snackBar.open($localize`Error Unarchiving Runs`); + } + }); + } + + private setRunsIsDeleted(runs: TeacherRun[], isDeleted: boolean): void { + for (const run of runs) { + run.project.isDeleted = isDeleted; + } + } + + runSelectedStatusChanged(): void { + this.updateSelectAllCheckboxAndNumRunsSelected(); + } + + runArchiveStatusChanged(): void { + this.performSearchAndFilter(); + } + + private getSelectedRuns(): TeacherRun[] { + return this.filteredRuns.filter((run: TeacherRun) => { + return run.isSelected; + }); + } + + private turnOnShowAll(): void { + this.showAll = true; + } } diff --git a/src/app/teacher/teacher-run.ts b/src/app/teacher/teacher-run.ts index 6eb4480b1a5..25889c4451f 100644 --- a/src/app/teacher/teacher-run.ts +++ b/src/app/teacher/teacher-run.ts @@ -1,6 +1,7 @@ import { Run } from '../domain/run'; export class TeacherRun extends Run { + isSelected: boolean; isHighlighted: boolean; shared: boolean; diff --git a/src/app/teacher/teacher.module.ts b/src/app/teacher/teacher.module.ts index 3cc451e5c50..2aeb1a75c84 100644 --- a/src/app/teacher/teacher.module.ts +++ b/src/app/teacher/teacher.module.ts @@ -41,6 +41,7 @@ import { DiscourseRecentActivityComponent } from './discourse-recent-activity/di import { ShareRunCodeDialogComponent } from './share-run-code-dialog/share-run-code-dialog.component'; import { MatToolbarModule } from '@angular/material/toolbar'; import { MatListModule } from '@angular/material/list'; +import { MatSlideToggleModule } from '@angular/material/slide-toggle'; const materialModules = [ MatAutocompleteModule, @@ -56,6 +57,7 @@ const materialModules = [ MatNativeDateModule, MatProgressBarModule, MatRadioModule, + MatSlideToggleModule, MatSnackBarModule, MatTabsModule, MatTableModule, diff --git a/src/app/teacher/teacher.service.ts b/src/app/teacher/teacher.service.ts index 0bcada405e6..b768abcb37a 100644 --- a/src/app/teacher/teacher.service.ts +++ b/src/app/teacher/teacher.service.ts @@ -65,6 +65,32 @@ export class TeacherService { return this.http.get(`${this.lastRunUrl}/${projectId}`); } + archiveRun(run: Run): Observable { + const params = new HttpParams().set('projectId', run.project.id); + return this.http.post(`/api/project/archive`, params); + } + + archiveRuns(runs: Run[]): Observable { + let params = new HttpParams(); + for (const run of runs) { + params = params.append('projectIds', run.project.id); + } + return this.http.post(`/api/project/archive/many`, params); + } + + unarchiveRuns(runs: Run[]): Observable { + let params = new HttpParams(); + for (const run of runs) { + params = params.append('projectIds', run.project.id); + } + return this.http.post(`/api/project/unarchive/many`, params); + } + + unarchiveRun(run: Run): Observable { + const params = new HttpParams().set('projectId', run.project.id); + return this.http.post(`/api/project/unarchive`, params); + } + registerTeacherAccount(teacherUser: Teacher): Observable { const headers = { 'Content-Type': 'application/json' diff --git a/src/messages.xlf b/src/messages.xlf index 27ed2e72c6b..ab219782cbd 100644 --- a/src/messages.xlf +++ b/src/messages.xlf @@ -2678,7 +2678,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/app/teacher/teacher-run-list/teacher-run-list.component.html - 17 + 18 src/assets/wise5/components/outsideURL/outside-url-authoring/outside-url-authoring.component.html @@ -5185,7 +5185,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html - 73 + 79 @@ -5864,7 +5864,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/app/teacher/teacher-run-list/teacher-run-list.component.html - 79 + 125 @@ -7236,7 +7236,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html - 110 + 119 @@ -7275,7 +7275,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/app/teacher/teacher-run-list/teacher-run-list.component.html - 40 + 36 @@ -7293,7 +7293,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/app/teacher/teacher-run-list/teacher-run-list.component.html - 47 + 44 @@ -7302,10 +7302,6 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.src/app/student/student-run-list/student-run-list.component.html 33 - - src/app/teacher/teacher-run-list/teacher-run-list.component.html - 50 - Clear search @@ -7913,18 +7909,60 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.26 + + Archive + + src/app/teacher/run-menu/run-menu.component.html + 35 + + + + Unarchive + + src/app/teacher/run-menu/run-menu.component.html + 39 + + Run Settings src/app/teacher/run-menu/run-menu.component.ts - 71 + 74 Edit Classroom Unit Warning src/app/teacher/run-menu/run-menu.component.ts - 81 + 84 + + + + Successfully Archived Run + + src/app/teacher/run-menu/run-menu.component.ts + 99 + + + + Error Archiving Run + + src/app/teacher/run-menu/run-menu.component.ts + 102 + + + + Successfully Unarchived Run + + src/app/teacher/run-menu/run-menu.component.ts + 113 + + + + Error Unarchiving Run + + src/app/teacher/run-menu/run-menu.component.ts + 116 @@ -8380,28 +8418,28 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.(Legacy Unit) src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html - 76 + 82 - - Last student login: + + Last student login: src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html - 101 + 110 Teacher Tools src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html - 121 + 130 Class Periods: src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.ts - 81 + 83 @@ -8418,18 +8456,39 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.3 - - Filter By Period + + Active src/app/teacher/teacher-run-list/teacher-run-list.component.html - 28 + 25 - - Total classroom units: + + Archived src/app/teacher/teacher-run-list/teacher-run-list.component.html - 43 + 30 + + + + Total classroom units: + + src/app/teacher/teacher-run-list/teacher-run-list.component.html + 39,40 + + + + running + + src/app/teacher/teacher-run-list/teacher-run-list.component.html + 47 + + + + completed + + src/app/teacher/teacher-run-list/teacher-run-list.component.html + 50 @@ -8439,23 +8498,97 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.56 - - All Periods + + All + + src/app/teacher/teacher-run-list/teacher-run-list.component.html + 76 + + + src/assets/wise5/classroomMonitor/dataExport/data-export/data-export.component.html + 458 + + + + None + + src/app/teacher/teacher-run-list/teacher-run-list.component.html + 77 + + + src/assets/wise5/authoringTool/peer-grouping/select-peer-grouping-option/select-peer-grouping-option.component.html + 18 + + + + Running + + src/app/teacher/teacher-run-list/teacher-run-list.component.html + 78 + + + + Completed + + src/app/teacher/teacher-run-list/teacher-run-list.component.html + 79 + + + src/assets/wise5/classroomMonitor/classroomMonitorComponents/milestones/milestone-details/milestone-details.component.html + 85 + + + src/assets/wise5/themes/default/themeComponents/nodeStatusIcon/node-status-icon.component.html + 20 + + + + Run(s) Selected + + src/app/teacher/teacher-run-list/teacher-run-list.component.html + 82,84 + + + + Archive Selected + + src/app/teacher/teacher-run-list/teacher-run-list.component.html + 91,93 + + + + Unarchive Selected + + src/app/teacher/teacher-run-list/teacher-run-list.component.html + 100,102 + + + + Successfully Archived Runs src/app/teacher/teacher-run-list/teacher-run-list.component.ts - 117 + 290 + + + Error Archiving Runs - src/assets/wise5/classroomMonitor/classroomMonitorComponents/nodeProgress/nav-item/nav-item.component.ts - 357 + src/app/teacher/teacher-run-list/teacher-run-list.component.ts + 293 + + + Successfully Unarchived Runs - src/assets/wise5/classroomMonitor/classroomMonitorComponents/select-period/select-period.component.ts - 96 + src/app/teacher/teacher-run-list/teacher-run-list.component.ts + 306 + + + Error Unarchiving Runs - src/assets/wise5/services/teacherDataService.ts - 637 + src/app/teacher/teacher-run-list/teacher-run-list.component.ts + 309 @@ -10033,13 +10166,6 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.14 - - None - - src/assets/wise5/authoringTool/peer-grouping/select-peer-grouping-option/select-peer-grouping-option.component.html - 18 - - Logic: @@ -10735,17 +10861,6 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.25 - - Completed - - src/assets/wise5/classroomMonitor/classroomMonitorComponents/milestones/milestone-details/milestone-details.component.html - 85 - - - src/assets/wise5/themes/default/themeComponents/nodeStatusIcon/node-status-icon.component.html - 20 - - Not Completed @@ -11128,6 +11243,21 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.241 + + All Periods + + src/assets/wise5/classroomMonitor/classroomMonitorComponents/nodeProgress/nav-item/nav-item.component.ts + 357 + + + src/assets/wise5/classroomMonitor/classroomMonitorComponents/select-period/select-period.component.ts + 96 + + + src/assets/wise5/services/teacherDataService.ts + 637 + + Period: @@ -11950,13 +12080,6 @@ Are you sure you want to proceed? 454 - - All - - src/assets/wise5/classroomMonitor/dataExport/data-export/data-export.component.html - 458 - - Download Latest Revisions Export From 340e7e1baf6e8f24b4836eade5928509e0ff3788 Mon Sep 17 00:00:00 2001 From: Geoffrey Kwan Date: Sun, 9 Apr 2023 12:30:52 -0400 Subject: [PATCH 02/43] chore(Run): Change order of run display to completed, running, scheduled #1012 --- .../teacher-run-list.component.html | 8 ++++---- src/app/teacher/teacher.service.ts | 10 +++++----- src/messages.xlf | 14 +++++++------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.html b/src/app/teacher/teacher-run-list/teacher-run-list.component.html index 39db03021b2..7ad58369bff 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.html +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.html @@ -40,14 +40,14 @@ {{ filteredRuns.length }} - ({{ scheduledTotal() }} scheduled, + ({{ completedTotal() }} completed, {{ activeTotal() }} running - , {{ completedTotal() }} completed, {{ scheduledTotal() }} scheduled) diff --git a/src/app/teacher/teacher.service.ts b/src/app/teacher/teacher.service.ts index b768abcb37a..0754fceaf17 100644 --- a/src/app/teacher/teacher.service.ts +++ b/src/app/teacher/teacher.service.ts @@ -78,6 +78,11 @@ export class TeacherService { return this.http.post(`/api/project/archive/many`, params); } + unarchiveRun(run: Run): Observable { + const params = new HttpParams().set('projectId', run.project.id); + return this.http.post(`/api/project/unarchive`, params); + } + unarchiveRuns(runs: Run[]): Observable { let params = new HttpParams(); for (const run of runs) { @@ -86,11 +91,6 @@ export class TeacherService { return this.http.post(`/api/project/unarchive/many`, params); } - unarchiveRun(run: Run): Observable { - const params = new HttpParams().set('projectId', run.project.id); - return this.http.post(`/api/project/unarchive`, params); - } - registerTeacherAccount(teacherUser: Teacher): Observable { const headers = { 'Content-Type': 'application/json' diff --git a/src/messages.xlf b/src/messages.xlf index ab219782cbd..b69b867ec46 100644 --- a/src/messages.xlf +++ b/src/messages.xlf @@ -7293,7 +7293,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/app/teacher/teacher-run-list/teacher-run-list.component.html - 44 + 50 @@ -8477,18 +8477,18 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.39,40 - - running + + completed src/app/teacher/teacher-run-list/teacher-run-list.component.html - 47 + 44 - - completed + + running src/app/teacher/teacher-run-list/teacher-run-list.component.html - 50 + 47 From 620eff31142ac37d63a642cc9c14e640e8144a9f Mon Sep 17 00:00:00 2001 From: Geoffrey Kwan Date: Thu, 22 Jun 2023 11:35:38 -0700 Subject: [PATCH 03/43] feat(Run): Change archiving to use tags instead of isDeleted field #1012 --- src/app/domain/project.ts | 1 - src/app/domain/run.ts | 1 + src/app/teacher/run-menu/run-menu.component.html | 4 ++-- src/app/teacher/run-menu/run-menu.component.ts | 4 ++-- .../teacher-run-list-item.component.html | 2 +- .../teacher-run-list.component.ts | 14 ++++++-------- src/app/teacher/teacher-run.ts | 1 + src/app/teacher/teacher.service.ts | 16 ++++++++-------- 8 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/app/domain/project.ts b/src/app/domain/project.ts index 47f76d86b06..6558c9ad945 100644 --- a/src/app/domain/project.ts +++ b/src/app/domain/project.ts @@ -18,7 +18,6 @@ export class Project { wiseVersion: number; uri: String; license: String; - isDeleted: boolean; static readonly VIEW_PERMISSION: number = 1; static readonly EDIT_PERMISSION: number = 2; diff --git a/src/app/domain/run.ts b/src/app/domain/run.ts index b521aefa2ba..b0f08c07898 100644 --- a/src/app/domain/run.ts +++ b/src/app/domain/run.ts @@ -15,6 +15,7 @@ export class Run { owner: User; sharedOwners: User[] = []; project: Project; + tags: string[]; static readonly VIEW_STUDENT_WORK_PERMISSION: number = 1; static readonly GRADE_AND_MANAGE_PERMISSION: number = 2; diff --git a/src/app/teacher/run-menu/run-menu.component.html b/src/app/teacher/run-menu/run-menu.component.html index 599f4ee8852..a8c59f4ee48 100644 --- a/src/app/teacher/run-menu/run-menu.component.html +++ b/src/app/teacher/run-menu/run-menu.component.html @@ -30,11 +30,11 @@ Report Problem - + folder Archive - + folder_off Unarchive diff --git a/src/app/teacher/run-menu/run-menu.component.ts b/src/app/teacher/run-menu/run-menu.component.ts index 5e9f39e45ff..97500eede7d 100644 --- a/src/app/teacher/run-menu/run-menu.component.ts +++ b/src/app/teacher/run-menu/run-menu.component.ts @@ -94,7 +94,7 @@ export class RunMenuComponent implements OnInit { const run = this.run; this.teacherService.archiveRun(run).subscribe({ next: () => { - run.project.isDeleted = true; + run.isArchived = true; this.runArchiveStatusChangedEvent.emit(run); this.snackBar.open($localize`Successfully Archived Run`); }, @@ -108,7 +108,7 @@ export class RunMenuComponent implements OnInit { const run = this.run; this.teacherService.unarchiveRun(run).subscribe({ next: () => { - run.project.isDeleted = false; + run.isArchived = false; this.runArchiveStatusChangedEvent.emit(run); this.snackBar.open($localize`Successfully Unarchived Run`); }, diff --git a/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html b/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html index 934e2ee95ec..844e82d140f 100644 --- a/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html +++ b/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html @@ -52,7 +52,7 @@ diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.ts b/src/app/teacher/teacher-run-list/teacher-run-list.component.ts index bad1264fc39..7ee873817f9 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.ts +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.ts @@ -75,6 +75,7 @@ export class TeacherRunListComponent implements OnInit { this.runs = runs.map((run) => { const teacherRun = new TeacherRun(run); teacherRun.shared = !teacherRun.isOwner(userId); + teacherRun.isArchived = teacherRun.tags.includes('archived'); return teacherRun; }); this.filteredRuns = this.runs; @@ -146,10 +147,7 @@ export class TeacherRunListComponent implements OnInit { private performFilter(): void { this.filteredRuns = this.filteredRuns.filter((run: TeacherRun) => { - return ( - (!this.isShowArchived && !run.project.isDeleted) || - (this.isShowArchived && run.project.isDeleted) - ); + return (!this.isShowArchived && !run.isArchived) || (this.isShowArchived && run.isArchived); }); } @@ -283,7 +281,7 @@ export class TeacherRunListComponent implements OnInit { const runs = this.getSelectedRuns(); return this.teacherService.archiveRuns(runs).subscribe({ next: () => { - this.setRunsIsDeleted(runs, true); + this.setRunsIsArchived(runs, true); this.unselectAllRuns(); this.updateSelectAllCheckboxAndNumRunsSelected(); this.performSearchAndFilter(); @@ -299,7 +297,7 @@ export class TeacherRunListComponent implements OnInit { const runs = this.getSelectedRuns(); return this.teacherService.unarchiveRuns(runs).subscribe({ next: () => { - this.setRunsIsDeleted(runs, false); + this.setRunsIsArchived(runs, false); this.unselectAllRuns(); this.updateSelectAllCheckboxAndNumRunsSelected(); this.performSearchAndFilter(); @@ -311,9 +309,9 @@ export class TeacherRunListComponent implements OnInit { }); } - private setRunsIsDeleted(runs: TeacherRun[], isDeleted: boolean): void { + private setRunsIsArchived(runs: TeacherRun[], isArchived: boolean): void { for (const run of runs) { - run.project.isDeleted = isDeleted; + run.isArchived = isArchived; } } diff --git a/src/app/teacher/teacher-run.ts b/src/app/teacher/teacher-run.ts index 25889c4451f..d3e23bd81f7 100644 --- a/src/app/teacher/teacher-run.ts +++ b/src/app/teacher/teacher-run.ts @@ -1,6 +1,7 @@ import { Run } from '../domain/run'; export class TeacherRun extends Run { + isArchived: boolean; isSelected: boolean; isHighlighted: boolean; shared: boolean; diff --git a/src/app/teacher/teacher.service.ts b/src/app/teacher/teacher.service.ts index 0754fceaf17..20442a02b30 100644 --- a/src/app/teacher/teacher.service.ts +++ b/src/app/teacher/teacher.service.ts @@ -66,29 +66,29 @@ export class TeacherService { } archiveRun(run: Run): Observable { - const params = new HttpParams().set('projectId', run.project.id); - return this.http.post(`/api/project/archive`, params); + const params = new HttpParams().set('runId', run.id); + return this.http.post(`/api/archive/run`, params); } archiveRuns(runs: Run[]): Observable { let params = new HttpParams(); for (const run of runs) { - params = params.append('projectIds', run.project.id); + params = params.append('runIds', run.id); } - return this.http.post(`/api/project/archive/many`, params); + return this.http.post(`/api/archive/run/many`, params); } unarchiveRun(run: Run): Observable { - const params = new HttpParams().set('projectId', run.project.id); - return this.http.post(`/api/project/unarchive`, params); + const params = new HttpParams().set('runId', run.id); + return this.http.post(`/api/unarchive/run`, params); } unarchiveRuns(runs: Run[]): Observable { let params = new HttpParams(); for (const run of runs) { - params = params.append('projectIds', run.project.id); + params = params.append('runIds', run.id); } - return this.http.post(`/api/project/unarchive/many`, params); + return this.http.post(`/api/unarchive/run/many`, params); } registerTeacherAccount(teacherUser: Teacher): Observable { From e6f23c9ebd962ecd95a4dcad2f3708c64f70ca63 Mon Sep 17 00:00:00 2001 From: Geoffrey Kwan Date: Fri, 11 Aug 2023 11:17:58 -0400 Subject: [PATCH 04/43] feat(Run): Move tags from run to project and use project id for archiving #1012 --- src/app/domain/project.ts | 23 ++++++++++--------- src/app/domain/run.ts | 1 - .../teacher-run-list.component.ts | 2 +- src/app/teacher/teacher.service.ts | 16 ++++++------- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/app/domain/project.ts b/src/app/domain/project.ts index 6558c9ad945..c44cd5130fd 100644 --- a/src/app/domain/project.ts +++ b/src/app/domain/project.ts @@ -2,22 +2,23 @@ import { Run } from './run'; import { User } from '../domain/user'; export class Project { - id: number; - name: string; - metadata: any; - dateCreated: string; dateArchived: string; - lastEdited: string; - projectThumb: string; - thumbStyle: any; + dateCreated: string; + id: number; isHighlighted: boolean; + lastEdited: string; + license: String; + metadata: any; + name: string; owner: User; - sharedOwners: User[] = []; - run: Run; parentId: number; - wiseVersion: number; + projectThumb: string; + run: Run; + sharedOwners: User[] = []; + tags: string[]; + thumbStyle: any; uri: String; - license: String; + wiseVersion: number; static readonly VIEW_PERMISSION: number = 1; static readonly EDIT_PERMISSION: number = 2; diff --git a/src/app/domain/run.ts b/src/app/domain/run.ts index b0f08c07898..b521aefa2ba 100644 --- a/src/app/domain/run.ts +++ b/src/app/domain/run.ts @@ -15,7 +15,6 @@ export class Run { owner: User; sharedOwners: User[] = []; project: Project; - tags: string[]; static readonly VIEW_STUDENT_WORK_PERMISSION: number = 1; static readonly GRADE_AND_MANAGE_PERMISSION: number = 2; diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.ts b/src/app/teacher/teacher-run-list/teacher-run-list.component.ts index 7ee873817f9..6d1050794bb 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.ts +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.ts @@ -75,7 +75,7 @@ export class TeacherRunListComponent implements OnInit { this.runs = runs.map((run) => { const teacherRun = new TeacherRun(run); teacherRun.shared = !teacherRun.isOwner(userId); - teacherRun.isArchived = teacherRun.tags.includes('archived'); + teacherRun.isArchived = teacherRun.project.tags.includes('archived'); return teacherRun; }); this.filteredRuns = this.runs; diff --git a/src/app/teacher/teacher.service.ts b/src/app/teacher/teacher.service.ts index 20442a02b30..f83ee79c99f 100644 --- a/src/app/teacher/teacher.service.ts +++ b/src/app/teacher/teacher.service.ts @@ -66,29 +66,29 @@ export class TeacherService { } archiveRun(run: Run): Observable { - const params = new HttpParams().set('runId', run.id); - return this.http.post(`/api/archive/run`, params); + const params = new HttpParams().set('projectId', run.project.id); + return this.http.post(`/api/archive/project`, params); } archiveRuns(runs: Run[]): Observable { let params = new HttpParams(); for (const run of runs) { - params = params.append('runIds', run.id); + params = params.append('projectIds', run.project.id); } - return this.http.post(`/api/archive/run/many`, params); + return this.http.post(`/api/archive/projects`, params); } unarchiveRun(run: Run): Observable { - const params = new HttpParams().set('runId', run.id); - return this.http.post(`/api/unarchive/run`, params); + const params = new HttpParams().set('projectId', run.project.id); + return this.http.post(`/api/unarchive/project`, params); } unarchiveRuns(runs: Run[]): Observable { let params = new HttpParams(); for (const run of runs) { - params = params.append('runIds', run.id); + params = params.append('projectIds', run.project.id); } - return this.http.post(`/api/unarchive/run/many`, params); + return this.http.post(`/api/unarchive/projects`, params); } registerTeacherAccount(teacherUser: Teacher): Observable { From cec8b1974801da9049aac4262e3ce1cf36d5c3ee Mon Sep 17 00:00:00 2001 From: Geoffrey Kwan Date: Fri, 11 Aug 2023 12:01:23 -0400 Subject: [PATCH 05/43] test(Archive): Fix archive run tests #1012 --- src/app/teacher/run-menu/run-menu.component.spec.ts | 4 ++-- .../teacher-run-list/teacher-run-list.component.spec.ts | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/app/teacher/run-menu/run-menu.component.spec.ts b/src/app/teacher/run-menu/run-menu.component.spec.ts index 7520900970d..c1f4e542346 100644 --- a/src/app/teacher/run-menu/run-menu.component.spec.ts +++ b/src/app/teacher/run-menu/run-menu.component.spec.ts @@ -112,7 +112,7 @@ function archive() { describe('archive()', () => { it('should archive a run', () => { component.archive(); - expect(component.run.project.isDeleted).toEqual(true); + expect(component.run.isArchived).toEqual(true); expect(snackBarSpy).toHaveBeenCalledWith('Successfully Archived Run'); }); }); @@ -122,7 +122,7 @@ function unarchive() { describe('unarchive()', () => { it('should unarchive a run', () => { component.unarchive(); - expect(component.run.project.isDeleted).toEqual(false); + expect(component.run.isArchived).toEqual(false); expect(snackBarSpy).toHaveBeenCalledWith('Successfully Unarchived Run'); }); }); diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts b/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts index da677195e66..9a8f8af0534 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts @@ -88,8 +88,9 @@ let teacherService: TeacherService; function createRun(id: number, ownerId: number): TeacherRun { return new TeacherRun({ id: id, - project: { id: id, isDeleted: false }, - owner: new User({ id: ownerId }) + project: { id: id, tags: [] }, + owner: new User({ id: ownerId }), + isArchived: false }); } @@ -181,7 +182,7 @@ function setRunsIsSelected(runs: TeacherRun[], isSelected: boolean[]): void { function expectRunsIsArchived(runs: TeacherRun[], isArchived: boolean[]): void { runs.forEach((run: TeacherRun, index: number) => { - expect(run.project.isDeleted).toEqual(isArchived[index]); + expect(run.isArchived).toEqual(isArchived[index]); }); } @@ -338,5 +339,5 @@ function runArchiveStatusChanged(): void { } function setRunIsArchived(run: TeacherRun, isArchived: boolean): void { - run.project.isDeleted = isArchived; + run.isArchived = isArchived; } From d1ca0b45ca35979769bd2edf504a44d8cf40b139 Mon Sep 17 00:00:00 2001 From: Geoffrey Kwan Date: Fri, 11 Aug 2023 12:35:21 -0400 Subject: [PATCH 06/43] feat(Archive): Allow archiving runs that are shared with you #1012 --- .../teacher/run-menu/run-menu.component.html | 18 ++++++++---------- .../teacher-run-list-item.component.html | 1 - 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/app/teacher/run-menu/run-menu.component.html b/src/app/teacher/run-menu/run-menu.component.html index a8c59f4ee48..8b907f0f8cc 100644 --- a/src/app/teacher/run-menu/run-menu.component.html +++ b/src/app/teacher/run-menu/run-menu.component.html @@ -29,15 +29,13 @@ report_problem Report Problem - - - folder - Archive - - - folder_off - Unarchive - - + + folder + Archive + + + folder_off + Unarchive + diff --git a/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html b/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html index cabd85952e9..343539ebc79 100644 --- a/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html +++ b/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html @@ -58,7 +58,6 @@ > Date: Fri, 11 Aug 2023 15:27:25 -0400 Subject: [PATCH 07/43] feat(Archive): Fix select runs drop down #1012 --- .../teacher-run-list.component.html | 15 +++++--- .../teacher-run-list.component.scss | 6 ++- .../teacher-run-list.component.ts | 3 -- src/messages.xlf | 38 +++++++++---------- 4 files changed, 33 insertions(+), 29 deletions(-) diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.html b/src/app/teacher/teacher-run-list/teacher-run-list.component.html index 7ad58369bff..f9661db5648 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.html +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.html @@ -72,12 +72,15 @@ (click)="selectAllRunsCheckboxClicked($event)" >
- - All - None - Running - Completed - + + + + + + +
{{ numSelectedRuns }} Run(s) Selected diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.scss b/src/app/teacher/teacher-run-list/teacher-run-list.component.scss index 185534a1e9e..869c56fcf3f 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.scss +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.scss @@ -41,4 +41,8 @@ h2 { font-weight: 500; font-size: 14px; margin-right: 24px; -} \ No newline at end of file +} + +.mat-icon { + margin: 0px; +} diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.ts b/src/app/teacher/teacher-run-list/teacher-run-list.component.ts index 6d1050794bb..dacc8c56f30 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.ts +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.ts @@ -213,7 +213,6 @@ export class TeacherRunListComponent implements OnInit { this.selectAllFilteredRuns(); } this.updateSelectAllCheckboxAndNumRunsSelected(); - event.preventDefault(); } private unselectAllRuns(): void { @@ -234,8 +233,6 @@ export class TeacherRunListComponent implements OnInit { selectRunsOptionChosen(value: string): void { this.turnOnShowAll(); - this.isSelectedAllRuns = value === 'all'; - this.isSelectedAllRuns = value === 'none'; this.filteredRuns .filter((run: TeacherRun) => !run.shared) .forEach((run: TeacherRun) => { diff --git a/src/messages.xlf b/src/messages.xlf index bb53516833d..8fb6f28e3b7 100644 --- a/src/messages.xlf +++ b/src/messages.xlf @@ -5480,7 +5480,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html - 80 + 79 @@ -6182,7 +6182,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/app/teacher/teacher-run-list/teacher-run-list.component.html - 125 + 128 @@ -7543,7 +7543,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html - 113 + 112 @@ -8224,14 +8224,14 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.Archive src/app/teacher/run-menu/run-menu.component.html - 35 + 34 Unarchive src/app/teacher/run-menu/run-menu.component.html - 39 + 38 @@ -8722,21 +8722,21 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.(Legacy Unit) src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html - 83 + 82 Last student login: src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html - 110 + 109 Teacher Tools src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html - 121 + 120 @@ -8806,7 +8806,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.All src/app/teacher/teacher-run-list/teacher-run-list.component.html - 76 + 79 src/assets/wise5/authoringTool/milestones-authoring/milestones-authoring.component.html @@ -8825,7 +8825,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.None src/app/teacher/teacher-run-list/teacher-run-list.component.html - 77 + 80 src/assets/wise5/authoringTool/peer-grouping/select-peer-grouping-option/select-peer-grouping-option.component.html @@ -8836,14 +8836,14 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.Running src/app/teacher/teacher-run-list/teacher-run-list.component.html - 78 + 81 Completed src/app/teacher/teacher-run-list/teacher-run-list.component.html - 79 + 82 src/assets/wise5/classroomMonitor/classroomMonitorComponents/milestones/milestone-details/milestone-details.component.html @@ -8858,49 +8858,49 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. Run(s) Selected src/app/teacher/teacher-run-list/teacher-run-list.component.html - 82,84 + 85,87 Archive Selected src/app/teacher/teacher-run-list/teacher-run-list.component.html - 91,93 + 94,96 Unarchive Selected src/app/teacher/teacher-run-list/teacher-run-list.component.html - 100,102 + 103,105 Successfully Archived Runs src/app/teacher/teacher-run-list/teacher-run-list.component.ts - 288 + 285 Error Archiving Runs src/app/teacher/teacher-run-list/teacher-run-list.component.ts - 291 + 288 Successfully Unarchived Runs src/app/teacher/teacher-run-list/teacher-run-list.component.ts - 304 + 301 Error Unarchiving Runs src/app/teacher/teacher-run-list/teacher-run-list.component.ts - 307 + 304 From c2d5c92f0607958bed4d157c4952315e29612bcb Mon Sep 17 00:00:00 2001 From: Geoffrey Kwan Date: Fri, 11 Aug 2023 15:37:06 -0400 Subject: [PATCH 08/43] test(Archive): Fix test #1012 --- .../teacher/teacher-run-list/teacher-run-list.component.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts b/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts index 9a8f8af0534..32a7d06f0b8 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts @@ -11,6 +11,7 @@ import { UserService } from '../../services/user.service'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { User } from '../../domain/user'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { MatMenuModule } from '@angular/material/menu'; class TeacherScheduleStubComponent {} @@ -104,6 +105,7 @@ describe('TeacherRunListComponent', () => { RouterTestingModule.withRoutes([ { path: 'teacher/home/schedule', component: TeacherScheduleStubComponent } ]), + MatMenuModule, MatSnackBarModule ], providers: [ From 523e9aa47d9f8ec8c84180f50a5bf874783c533a Mon Sep 17 00:00:00 2001 From: Geoffrey Kwan Date: Fri, 11 Aug 2023 18:06:01 -0400 Subject: [PATCH 09/43] feat(Archive): Create component for select runs checkbox and drop down #1012 --- .../select-runs-controls.component.html | 20 +++++ .../select-runs-controls.component.scss | 11 +++ .../select-runs-controls.component.spec.ts | 25 ++++++ .../select-runs-controls.component.ts | 41 ++++++++++ .../teacher-run-list.component.html | 29 ++----- .../teacher-run-list.component.scss | 12 --- .../teacher-run-list.component.spec.ts | 48 +---------- .../teacher-run-list.component.ts | 81 +++++-------------- src/app/teacher/teacher.module.ts | 2 + 9 files changed, 127 insertions(+), 142 deletions(-) create mode 100644 src/app/teacher/select-runs-controls/select-runs-controls.component.html create mode 100644 src/app/teacher/select-runs-controls/select-runs-controls.component.scss create mode 100644 src/app/teacher/select-runs-controls/select-runs-controls.component.spec.ts create mode 100644 src/app/teacher/select-runs-controls/select-runs-controls.component.ts diff --git a/src/app/teacher/select-runs-controls/select-runs-controls.component.html b/src/app/teacher/select-runs-controls/select-runs-controls.component.html new file mode 100644 index 00000000000..0c841ecdc3c --- /dev/null +++ b/src/app/teacher/select-runs-controls/select-runs-controls.component.html @@ -0,0 +1,20 @@ +
+ +
+ + + + + + + +
+
diff --git a/src/app/teacher/select-runs-controls/select-runs-controls.component.scss b/src/app/teacher/select-runs-controls/select-runs-controls.component.scss new file mode 100644 index 00000000000..8330b6a475a --- /dev/null +++ b/src/app/teacher/select-runs-controls/select-runs-controls.component.scss @@ -0,0 +1,11 @@ +.select-all-check-box { + margin-right: 6px; +} + +.select-all-drop-down { + margin-right: 24px; +} + +.mat-icon { + margin: 0px; +} \ No newline at end of file diff --git a/src/app/teacher/select-runs-controls/select-runs-controls.component.spec.ts b/src/app/teacher/select-runs-controls/select-runs-controls.component.spec.ts new file mode 100644 index 00000000000..86b4d23d86e --- /dev/null +++ b/src/app/teacher/select-runs-controls/select-runs-controls.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SelectRunsControlsComponent } from './select-runs-controls.component'; +import { MatMenuModule } from '@angular/material/menu'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatIconModule } from '@angular/material/icon'; + +describe('SelectRunsControlsComponent', () => { + let component: SelectRunsControlsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [SelectRunsControlsComponent], + imports: [MatCheckboxModule, MatIconModule, MatMenuModule] + }).compileComponents(); + + fixture = TestBed.createComponent(SelectRunsControlsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/teacher/select-runs-controls/select-runs-controls.component.ts b/src/app/teacher/select-runs-controls/select-runs-controls.component.ts new file mode 100644 index 00000000000..06e2fd89adb --- /dev/null +++ b/src/app/teacher/select-runs-controls/select-runs-controls.component.ts @@ -0,0 +1,41 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { MAT_CHECKBOX_DEFAULT_OPTIONS } from '@angular/material/checkbox'; + +@Component({ + selector: 'select-runs-controls', + templateUrl: './select-runs-controls.component.html', + styleUrls: ['./select-runs-controls.component.scss'], + providers: [{ provide: MAT_CHECKBOX_DEFAULT_OPTIONS, useValue: { clickAction: 'noop' } }] +}) +export class SelectRunsControlsComponent { + isSelectedAllRuns: boolean = false; + isSelectedSomeRuns: boolean = false; + @Input() numSelectedRuns: number = 0; + @Input() numTotalRuns: number = 0; + @Output() selectRunsOptionChosenEvent = new EventEmitter(); + + ngOnChanges(): void { + if (this.numSelectedRuns === 0) { + this.isSelectedAllRuns = false; + this.isSelectedSomeRuns = false; + } else if (this.numSelectedRuns === this.numTotalRuns) { + this.isSelectedAllRuns = true; + this.isSelectedSomeRuns = false; + } else { + this.isSelectedAllRuns = false; + this.isSelectedSomeRuns = true; + } + } + + protected selectAllRunsCheckboxClicked(): void { + if (this.isSelectedAllRuns || this.isSelectedSomeRuns) { + this.selectRunsOptionChosenEvent.emit('none'); + } else { + this.selectRunsOptionChosenEvent.emit('all'); + } + } + + protected selectRunsOptionChosen(value: string): void { + this.selectRunsOptionChosenEvent.emit(value); + } +} diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.html b/src/app/teacher/teacher-run-list/teacher-run-list.component.html index f9661db5648..cc28623b54e 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.html +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.html @@ -63,32 +63,20 @@ class="select-all-controls" fxLayout="row" fxLayoutAlign="start center" + fxLayoutGap="20px" > - -
- - - - - - - -
+
{{ numSelectedRuns }} Run(s) Selected
diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.scss b/src/app/teacher/teacher-run-list/teacher-run-list.component.scss index 869c56fcf3f..3c20207ab36 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.scss +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.scss @@ -29,20 +29,8 @@ h2 { height: 36px; } -.select-all-check-box { - margin-right: 6px; -} - -.select-all-drop-down { - margin-right: 24px; -} - .num-selected-runs-display { font-weight: 500; font-size: 14px; margin-right: 24px; } - -.mat-icon { - margin: 0px; -} diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts b/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts index 32a7d06f0b8..3e23a18f4b8 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts @@ -1,3 +1,4 @@ +// @ts-nocheck import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { defer, Observable, of } from 'rxjs'; import { TeacherRunListComponent } from './teacher-run-list.component'; @@ -11,7 +12,6 @@ import { UserService } from '../../services/user.service'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { User } from '../../domain/user'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { MatMenuModule } from '@angular/material/menu'; class TeacherScheduleStubComponent {} @@ -105,7 +105,6 @@ describe('TeacherRunListComponent', () => { RouterTestingModule.withRoutes([ { path: 'teacher/home/schedule', component: TeacherScheduleStubComponent } ]), - MatMenuModule, MatSnackBarModule ], providers: [ @@ -137,7 +136,6 @@ describe('TeacherRunListComponent', () => { isShowArchiveChanged(); runArchiveStatusChanged(); runSelectedStatusChanged(); - selectAllRunsCheckboxClicked(); selectRunsOptionChosen(); sortByStartTimeDesc(); unarchiveSelectedRuns(); @@ -217,54 +215,13 @@ function isShowArchiveChanged(): void { describe('active runs are shown and some runs are selected', () => { it('should unselect the runs', () => { setRunsIsSelected([run1, run2, run3], [true, false, true]); - setAllRunsCheckbox(false, true); component.isShowArchivedChanged(); expectRunsIsSelected([run1, run2, run3], [false, false, false]); - expectSelectAllRunsCheckbox(false, false); }); }); }); } -function selectAllRunsCheckboxClicked(): void { - describe('selectAllRunsCheckboxClicked()', () => { - describe('select all runs checkbox is not checked', () => { - it('when select all runs checkbox is clicked it should select all runs', () => { - setAllRunsCheckbox(false, false); - component.selectAllRunsCheckboxClicked(dummyClickEvent); - expectSelectAllRunsCheckbox(true, false); - }); - }); - describe('select all runs checkbox is checked', () => { - it('when select all runs checkbox is clicked it should unselect all runs', () => { - setAllRunsCheckbox(true, false); - component.selectAllRunsCheckboxClicked(dummyClickEvent); - expectSelectAllRunsCheckbox(false, false); - }); - }); - describe('select all runs checkbox is indeterminate checked', () => { - it('when select all runs checkbox is clicked it should unselect all runs', () => { - setAllRunsCheckbox(false, true); - component.selectAllRunsCheckboxClicked(dummyClickEvent); - expectSelectAllRunsCheckbox(false, false); - }); - }); - }); -} - -function setAllRunsCheckbox(isSelectedAllRuns: boolean, isSelectedSomeRuns: boolean): void { - component.isSelectedAllRuns = isSelectedAllRuns; - component.isSelectedSomeRuns = isSelectedSomeRuns; -} - -function expectSelectAllRunsCheckbox( - isSelectedAllRuns: boolean, - isSelectedSomeRuns: boolean -): void { - expect(component.isSelectedAllRuns).toEqual(isSelectedAllRuns); - expect(component.isSelectedSomeRuns).toEqual(isSelectedSomeRuns); -} - function runSelectedStatusChanged(): void { describe('runSelectedStatusChanged()', () => { describe('one run is selected', () => { @@ -272,7 +229,6 @@ function runSelectedStatusChanged(): void { setRunsIsSelected([run1, run2, run3], [true, false, false]); component.runSelectedStatusChanged(); expect(component.numSelectedRuns).toEqual(1); - expectSelectAllRunsCheckbox(false, true); }); }); describe('two runs are selected', () => { @@ -280,7 +236,6 @@ function runSelectedStatusChanged(): void { setRunsIsSelected([run1, run2, run3], [true, true, false]); component.runSelectedStatusChanged(); expect(component.numSelectedRuns).toEqual(2); - expectSelectAllRunsCheckbox(false, true); }); }); describe('all runs are selected', () => { @@ -288,7 +243,6 @@ function runSelectedStatusChanged(): void { setRunsIsSelected([run1, run2, run3], [true, true, true]); component.runSelectedStatusChanged(); expect(component.numSelectedRuns).toEqual(3); - expectSelectAllRunsCheckbox(true, false); }); }); }); diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.ts b/src/app/teacher/teacher-run-list/teacher-run-list.component.ts index dacc8c56f30..a8fe13c8d7c 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.ts +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.ts @@ -23,8 +23,6 @@ export class TeacherRunListComponent implements OnInit { searchValue: string = ''; filterOptions: any[]; filterValue: string = ''; - isSelectedAllRuns: boolean = false; - isSelectedSomeRuns: boolean = false; isShowArchived: boolean = false; numSelectedRuns: number = 0; showAll: boolean = false; @@ -101,29 +99,29 @@ export class TeacherRunListComponent implements OnInit { this.performSearchAndFilter(); } - sortByStartTimeDesc(a: TeacherRun, b: TeacherRun): number { + protected sortByStartTimeDesc(a: TeacherRun, b: TeacherRun): number { return b.startTime - a.startTime; } - runSpansDays(run: TeacherRun): boolean { + protected runSpansDays(run: TeacherRun): boolean { const startDay = formatDate(run.startTime, 'shortDate', this.localeID); const endDay = formatDate(run.endTime, 'shortDate', this.localeID); return startDay != endDay; } - activeTotal(): number { + protected activeTotal(): number { const now = this.configService.getCurrentServerTime(); return this.filteredRuns.filter((run: TeacherRun) => run.isActive(now)).length; } - completedTotal(): number { + protected completedTotal(): number { const now = this.configService.getCurrentServerTime(); return this.filteredRuns.filter( (run: TeacherRun) => !run.isActive(now) && !run.isScheduled(now) ).length; } - scheduledTotal(): number { + protected scheduledTotal(): number { const now = this.configService.getCurrentServerTime(); return this.filteredRuns.filter((run: TeacherRun) => run.isScheduled(now)).length; } @@ -134,17 +132,12 @@ export class TeacherRunListComponent implements OnInit { this.updateNumSelectedRuns(); } - searchChanged(searchValue: string): void { + protected searchChanged(searchValue: string): void { this.searchValue = searchValue; this.performSearchAndFilter(); this.turnOnShowAll(); } - filterChanged(value: string): void { - this.filterValue = value; - this.performSearchAndFilter(); - } - private performFilter(): void { this.filteredRuns = this.filteredRuns.filter((run: TeacherRun) => { return (!this.isShowArchived && !run.isArchived) || (this.isShowArchived && run.isArchived); @@ -167,13 +160,13 @@ export class TeacherRunListComponent implements OnInit { ); } - reset(): void { + protected reset(): void { this.searchValue = ''; this.filterValue = ''; this.performSearchAndFilter(); } - isRunActive(run: TeacherRun): boolean { + protected isRunActive(run: TeacherRun): boolean { return run.isActive(this.configService.getCurrentServerTime()); } @@ -198,40 +191,20 @@ export class TeacherRunListComponent implements OnInit { } } - isShowArchivedChanged(): void { + protected isShowArchivedChanged(): void { this.turnOnShowAll(); this.unselectAllRuns(); - this.updateSelectAllCheckboxAndNumRunsSelected(); + this.updateNumSelectedRuns(); this.performSearchAndFilter(); } - selectAllRunsCheckboxClicked(event: any): void { - this.turnOnShowAll(); - if (this.isSelectedAllRuns || this.isSelectedSomeRuns) { - this.unselectAllRuns(); - } else { - this.selectAllFilteredRuns(); - } - this.updateSelectAllCheckboxAndNumRunsSelected(); - } - private unselectAllRuns(): void { - this.isSelectedAllRuns = false; - this.isSelectedSomeRuns = false; for (const run of this.runs) { run.isSelected = false; } } - private selectAllFilteredRuns(): void { - this.filteredRuns - .filter((run: TeacherRun) => !run.shared) - .forEach((run: TeacherRun) => { - run.isSelected = true; - }); - } - - selectRunsOptionChosen(value: string): void { + protected selectRunsOptionChosen(value: string): void { this.turnOnShowAll(); this.filteredRuns .filter((run: TeacherRun) => !run.shared) @@ -251,36 +224,20 @@ export class TeacherRunListComponent implements OnInit { break; } }); - this.updateSelectAllCheckboxAndNumRunsSelected(); - } - - private updateSelectAllCheckboxAndNumRunsSelected(): void { - this.updateSelectAllCheckbox(); this.updateNumSelectedRuns(); } - private updateSelectAllCheckbox(): void { - const numFilteredRuns = this.filteredRuns.length; - const numSelectedRuns = this.getNumSelectedRuns(); - this.isSelectedAllRuns = numSelectedRuns > 0 && numSelectedRuns === numFilteredRuns; - this.isSelectedSomeRuns = numSelectedRuns > 0 && numSelectedRuns !== numFilteredRuns; - } - private updateNumSelectedRuns(): void { - this.numSelectedRuns = this.getNumSelectedRuns(); + this.numSelectedRuns = this.getSelectedRuns().length; } - private getNumSelectedRuns(): number { - return this.getSelectedRuns().length; - } - - archiveSelectedRuns(): Subscription { + protected archiveSelectedRuns(): Subscription { const runs = this.getSelectedRuns(); return this.teacherService.archiveRuns(runs).subscribe({ next: () => { this.setRunsIsArchived(runs, true); this.unselectAllRuns(); - this.updateSelectAllCheckboxAndNumRunsSelected(); + this.updateNumSelectedRuns(); this.performSearchAndFilter(); this.snackBar.open($localize`Successfully Archived ${runs.length} Runs`); }, @@ -290,13 +247,13 @@ export class TeacherRunListComponent implements OnInit { }); } - unarchiveSelectedRuns(): Subscription { + protected unarchiveSelectedRuns(): Subscription { const runs = this.getSelectedRuns(); return this.teacherService.unarchiveRuns(runs).subscribe({ next: () => { this.setRunsIsArchived(runs, false); this.unselectAllRuns(); - this.updateSelectAllCheckboxAndNumRunsSelected(); + this.updateNumSelectedRuns(); this.performSearchAndFilter(); this.snackBar.open($localize`Successfully Unarchived ${runs.length} Runs`); }, @@ -312,11 +269,11 @@ export class TeacherRunListComponent implements OnInit { } } - runSelectedStatusChanged(): void { - this.updateSelectAllCheckboxAndNumRunsSelected(); + protected runSelectedStatusChanged(): void { + this.updateNumSelectedRuns(); } - runArchiveStatusChanged(): void { + protected runArchiveStatusChanged(): void { this.performSearchAndFilter(); } diff --git a/src/app/teacher/teacher.module.ts b/src/app/teacher/teacher.module.ts index 2f2d697e30f..527aac50eff 100644 --- a/src/app/teacher/teacher.module.ts +++ b/src/app/teacher/teacher.module.ts @@ -41,6 +41,7 @@ import { ShareRunCodeDialogComponent } from './share-run-code-dialog/share-run-c import { MatToolbarModule } from '@angular/material/toolbar'; import { MatListModule } from '@angular/material/list'; import { MatSlideToggleModule } from '@angular/material/slide-toggle'; +import { SelectRunsControlsComponent } from './select-runs-controls/select-runs-controls.component'; const materialModules = [ MatAutocompleteModule, @@ -84,6 +85,7 @@ const materialModules = [ ListClassroomCoursesDialogComponent, RunMenuComponent, RunSettingsDialogComponent, + SelectRunsControlsComponent, ShareRunCodeDialogComponent, ShareRunDialogComponent, TeacherComponent, From 55551cd43d999c512484dd31920d4ab47a0fe99c Mon Sep 17 00:00:00 2001 From: Geoffrey Kwan Date: Thu, 17 Aug 2023 11:44:29 -0400 Subject: [PATCH 10/43] test(Archive): Add test harnesses to teacher run list tests #1012 --- .../select-runs-controls.component.html | 2 +- .../select-runs-controls.component.scss | 2 +- .../select-runs-controls.component.spec.ts | 7 +- .../select-runs-controls.harness.ts | 40 +++ .../select-runs-controls.module.ts | 14 + .../teacher-run-list-item.harness.ts | 25 ++ .../teacher-run-list.component.spec.ts | 305 +++++++++--------- .../teacher-run-list.harness.ts | 86 +++++ src/app/teacher/teacher.module.ts | 4 +- 9 files changed, 331 insertions(+), 154 deletions(-) create mode 100644 src/app/teacher/select-runs-controls/select-runs-controls.harness.ts create mode 100644 src/app/teacher/select-runs-controls/select-runs-controls.module.ts create mode 100644 src/app/teacher/teacher-run-list-item/teacher-run-list-item.harness.ts create mode 100644 src/app/teacher/teacher-run-list/teacher-run-list.harness.ts diff --git a/src/app/teacher/select-runs-controls/select-runs-controls.component.html b/src/app/teacher/select-runs-controls/select-runs-controls.component.html index 0c841ecdc3c..de0ee54c8c5 100644 --- a/src/app/teacher/select-runs-controls/select-runs-controls.component.html +++ b/src/app/teacher/select-runs-controls/select-runs-controls.component.html @@ -1,6 +1,6 @@
{ let component: SelectRunsControlsComponent; @@ -10,8 +8,7 @@ describe('SelectRunsControlsComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [SelectRunsControlsComponent], - imports: [MatCheckboxModule, MatIconModule, MatMenuModule] + imports: [SelectRunsControlsModule] }).compileComponents(); fixture = TestBed.createComponent(SelectRunsControlsComponent); diff --git a/src/app/teacher/select-runs-controls/select-runs-controls.harness.ts b/src/app/teacher/select-runs-controls/select-runs-controls.harness.ts new file mode 100644 index 00000000000..988f4ca7a50 --- /dev/null +++ b/src/app/teacher/select-runs-controls/select-runs-controls.harness.ts @@ -0,0 +1,40 @@ +import { ComponentHarness } from '@angular/cdk/testing'; +import { MatCheckboxHarness } from '@angular/material/checkbox/testing'; +import { MatMenuHarness } from '@angular/material/menu/testing'; + +export class SelectRunsControlsHarness extends ComponentHarness { + static hostSelector = 'select-runs-controls'; + protected getMenu = this.locatorFor(MatMenuHarness); + protected getSelectAllCheckbox = this.locatorFor(MatCheckboxHarness); + + async checkCheckbox(): Promise { + const selectAllCheckBox = await this.getSelectAllCheckbox(); + return await selectAllCheckBox.check(); + } + + async uncheckCheckbox(): Promise { + const selectAllCheckBox = await this.getSelectAllCheckbox(); + return await selectAllCheckBox.uncheck(); + } + + async toggleCheckbox(): Promise { + const selectAllCheckBox = await this.getSelectAllCheckbox(); + return await selectAllCheckBox.toggle(); + } + + async isChecked(): Promise { + const checkbox = await this.getSelectAllCheckbox(); + return await checkbox.isChecked(); + } + + async isIndeterminate(): Promise { + const checkbox = await this.getSelectAllCheckbox(); + return await checkbox.isIndeterminate(); + } + + async clickMenuButton(menuButtonText: string): Promise { + const menu = await this.getMenu(); + await menu.open(); + return await menu.clickItem({ text: menuButtonText }); + } +} diff --git a/src/app/teacher/select-runs-controls/select-runs-controls.module.ts b/src/app/teacher/select-runs-controls/select-runs-controls.module.ts new file mode 100644 index 00000000000..ed3b56eeb20 --- /dev/null +++ b/src/app/teacher/select-runs-controls/select-runs-controls.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatIconModule } from '@angular/material/icon'; +import { MatMenuModule } from '@angular/material/menu'; +import { SelectRunsControlsComponent } from './select-runs-controls.component'; +import { FlexLayoutModule } from '@angular/flex-layout'; +import { MatButtonModule } from '@angular/material/button'; + +@NgModule({ + declarations: [SelectRunsControlsComponent], + exports: [SelectRunsControlsComponent], + imports: [FlexLayoutModule, MatButtonModule, MatCheckboxModule, MatIconModule, MatMenuModule] +}) +export class SelectRunsControlsModule {} diff --git a/src/app/teacher/teacher-run-list-item/teacher-run-list-item.harness.ts b/src/app/teacher/teacher-run-list-item/teacher-run-list-item.harness.ts new file mode 100644 index 00000000000..bd924ff8dbe --- /dev/null +++ b/src/app/teacher/teacher-run-list-item/teacher-run-list-item.harness.ts @@ -0,0 +1,25 @@ +import { ComponentHarness } from '@angular/cdk/testing'; +import { MatCheckboxHarness } from '@angular/material/checkbox/testing'; +import { MatMenuHarness } from '@angular/material/menu/testing'; + +export class TeacherRunListItemHarness extends ComponentHarness { + static hostSelector = 'app-teacher-run-list-item'; + protected getCheckbox = this.locatorFor(MatCheckboxHarness); + protected getMenu = this.locatorFor(MatMenuHarness); + + async checkCheckbox(): Promise { + const checkbox = await this.getCheckbox(); + return await checkbox.check(); + } + + async isChecked(): Promise { + const checkbox = await this.getCheckbox(); + return await checkbox.isChecked(); + } + + async clickMenuButton(menuButtonText: string): Promise { + const menu = await this.getMenu(); + await menu.open(); + return await menu.clickItem({ text: menuButtonText }); + } +} diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts b/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts index 3e23a18f4b8..947f05b0ef7 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts @@ -1,9 +1,7 @@ -// @ts-nocheck import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { defer, Observable, of } from 'rxjs'; +import { of } from 'rxjs'; import { TeacherRunListComponent } from './teacher-run-list.component'; import { TeacherService } from '../teacher.service'; -import { Project } from '../../domain/project'; import { TeacherRun } from '../teacher-run'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ConfigService } from '../../services/config.service'; @@ -12,86 +10,48 @@ import { UserService } from '../../services/user.service'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { User } from '../../domain/user'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { SelectRunsControlsModule } from '../select-runs-controls/select-runs-controls.module'; +import { BrowserModule } from '@angular/platform-browser'; +import { TeacherRunListItemComponent } from '../teacher-run-list-item/teacher-run-list-item.component'; +import { MatDialogModule } from '@angular/material/dialog'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { TeacherRunListHarness } from './teacher-run-list.harness'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatInputModule } from '@angular/material/input'; +import { FormsModule } from '@angular/forms'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatSlideToggleModule } from '@angular/material/slide-toggle'; +import { RunMenuComponent } from '../run-menu/run-menu.component'; +import { MatMenuModule } from '@angular/material/menu'; class TeacherScheduleStubComponent {} -export function fakeAsyncResponse(data: T) { - return defer(() => Promise.resolve(data)); -} - -export class MockTeacherService { - archiveRuns(): Observable { - return of([]); - } - unarchiveRuns(): Observable { - return of([]); - } - getRuns(): Observable { - const runs: TeacherRun[] = []; - const run1 = new TeacherRun(); - run1.id = 1; - run1.name = 'Photosynthesis'; - run1.numStudents = 30; - run1.periods = ['1', '2']; - run1.startTime = new Date('2018-01-01T00:00:00.0').getTime(); - const project1 = new Project(); - project1.id = 1; - project1.name = 'Photosynthesis'; - project1.projectThumb = ''; - run1.project = project1; - const run2 = new TeacherRun(); - run2.id = 2; - run2.name = 'Plate Tectonics'; - run2.numStudents = 15; - run2.periods = ['3', '4']; - run2.startTime = new Date('2018-03-03T00:00:00.0').getTime(); - const project2 = new Project(); - project2.id = 1; - project2.name = 'Plate Tectonics'; - project2.projectThumb = ''; - run2.project = project2; - runs.push(run1); - runs.push(run2); - return Observable.create((observer) => { - observer.next(runs); - observer.complete(); - }); - } - runs$ = fakeAsyncResponse({ - id: 3, - name: 'Global Climate Change', - periods: ['1', '2'] - }); -} - -export class MockConfigService { - getCurrentServerTime(): number { - return new Date('2018-08-24T00:00:00.0').getTime(); - } -} - -export class MockUserService { - getUserId(): number { - return 1; - } -} - let component: TeacherRunListComponent; let configService: ConfigService; const currentTime = new Date().getTime(); -const dummyClickEvent: any = { preventDefault: () => {} }; let fixture: ComponentFixture; let run1: TeacherRun; let run2: TeacherRun; let run3: TeacherRun; +let runListHarness: TeacherRunListHarness; let teacherService: TeacherService; +let userService: UserService; -function createRun(id: number, ownerId: number): TeacherRun { +function createRun(id: number, ownerId: number, startTime: number): TeacherRun { return new TeacherRun({ id: id, - project: { id: id, tags: [] }, + isArchived: false, + isSelected: false, + numStudents: 10, owner: new User({ id: ownerId }), - isArchived: false + periods: [], + project: { + id: id, + tags: [], + owner: new User({ id: ownerId }) + }, + startTime: startTime }); } @@ -99,43 +59,77 @@ describe('TeacherRunListComponent', () => { beforeEach( waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [TeacherRunListComponent], + declarations: [RunMenuComponent, TeacherRunListComponent, TeacherRunListItemComponent], imports: [ BrowserAnimationsModule, + BrowserModule, + FormsModule, + HttpClientTestingModule, + MatCheckboxModule, + MatDialogModule, + MatFormFieldModule, + MatInputModule, + MatMenuModule, + MatSlideToggleModule, + MatSnackBarModule, RouterTestingModule.withRoutes([ { path: 'teacher/home/schedule', component: TeacherScheduleStubComponent } ]), - MatSnackBarModule - ], - providers: [ - { provide: TeacherService, useClass: MockTeacherService }, - { provide: ConfigService, useClass: MockConfigService }, - { provide: UserService, useClass: MockUserService } + SelectRunsControlsModule ], + providers: [TeacherService, ConfigService, UserService], schemas: [NO_ERRORS_SCHEMA] }); }) ); - beforeEach(() => { - fixture = TestBed.createComponent(TeacherRunListComponent); + beforeEach(async () => { configService = TestBed.inject(ConfigService); teacherService = TestBed.inject(TeacherService); - component = fixture.componentInstance; + userService = TestBed.inject(UserService); spyOn(teacherService, 'getRuns').and.returnValue( - of([createRun(1, 1), createRun(2, 1), createRun(3, 1)]) + of([ + createRun(1, 1, new Date('2020-01-01').getTime()), + createRun(2, 1, new Date('2020-01-02').getTime()), + createRun(3, 1, new Date('2020-01-03').getTime()) + ]) ); spyOn(configService, 'getCurrentServerTime').and.returnValue(currentTime); + spyOn(configService, 'getContextPath').and.returnValue(''); + spyOn(userService, 'getUserId').and.returnValue(1); + spyOn(teacherService, 'archiveRun').and.callFake((run: TeacherRun) => { + run.isArchived = true; + return of(run); + }); + spyOn(teacherService, 'archiveRuns').and.callFake((runs: TeacherRun[]) => { + runs.forEach((run) => (run.isArchived = true)); + return of(runs); + }); + spyOn(teacherService, 'unarchiveRun').and.callFake((run: TeacherRun) => { + run.isArchived = false; + return of(run); + }); + spyOn(teacherService, 'unarchiveRuns').and.callFake((runs: TeacherRun[]) => { + runs.forEach((run) => (run.isArchived = false)); + return of(runs); + }); + fixture = TestBed.createComponent(TeacherRunListComponent); + component = fixture.componentInstance; fixture.detectChanges(); run1 = component.runs[0]; run2 = component.runs[1]; run3 = component.runs[2]; + runListHarness = await TestbedHarnessEnvironment.harnessForFixture( + fixture, + TeacherRunListHarness + ); }); archiveSelectedRuns(); isShowArchiveChanged(); runArchiveStatusChanged(); runSelectedStatusChanged(); + selectAllRunsCheckboxClicked(); selectRunsOptionChosen(); sortByStartTimeDesc(); unarchiveSelectedRuns(); @@ -144,20 +138,7 @@ describe('TeacherRunListComponent', () => { function sortByStartTimeDesc() { describe('sortByStartTimeDesc()', () => { it('should sort runs by start date', () => { - const run3 = new TeacherRun(); - run3.id = 3; - run3.name = 'Planet Earth'; - run3.numStudents = 10; - run3.periods = ['6', '7']; - run3.startTime = new Date('2018-02-02T00:00:00.0').getTime(); - const project3 = new Project(); - project3.id = 1; - project3.name = 'Planet Earth'; - project3.projectThumb = ''; - run3.project = project3; - component.runs.push(run3); - component.runs.sort(component.sortByStartTimeDesc); - expect(isRunsSortedByStartTimeDesc(component.runs)).toBeTruthy(); + expect(isRunsSortedByStartTimeDesc(component.filteredRuns)).toBeTruthy(); }); }); } @@ -174,36 +155,27 @@ function isRunsSortedByStartTimeDesc(runs: TeacherRun[]): boolean { return true; } -function setRunsIsSelected(runs: TeacherRun[], isSelected: boolean[]): void { - runs.forEach((run: TeacherRun, index: number) => { - run.isSelected = isSelected[index]; - }); -} - -function expectRunsIsArchived(runs: TeacherRun[], isArchived: boolean[]): void { - runs.forEach((run: TeacherRun, index: number) => { - expect(run.isArchived).toEqual(isArchived[index]); - }); -} - function archiveSelectedRuns(): void { describe('archiveSelectedRuns()', () => { - it('should archive selected runs', () => { - setRunsIsSelected([run1, run2, run3], [true, true, false]); - spyOn(teacherService, 'archiveRuns').and.returnValue(of([run1, run2])); - component.archiveSelectedRuns(); + it('should archive selected runs', async () => { + await runListHarness.clickRunListItemCheckbox(0); + await runListHarness.clickRunListItemCheckbox(1); + await runListHarness.clickArchiveButton(); expectRunsIsSelected([run1, run2, run3], [false, false, false]); - expectRunsIsArchived([run1, run2, run3], [true, true, false]); + expectRunsIsArchived([run1, run2, run3], [false, true, true]); }); }); } function unarchiveSelectedRuns(): void { describe('unarchiveSelectedRuns()', () => { - it('should unarchive selected runs', () => { - setRunsIsSelected([run1, run2, run3], [true, true, false]); - spyOn(teacherService, 'unarchiveRuns').and.returnValue(of([run1, run2])); - component.unarchiveSelectedRuns(); + it('should unarchive selected runs', async () => { + run1.isArchived = true; + run2.isArchived = true; + await runListHarness.toggleArchiveToggle(); + await runListHarness.clickRunListItemCheckbox(0); + await runListHarness.clickRunListItemCheckbox(1); + await runListHarness.clickUnarchiveButton(); expectRunsIsSelected([run1, run2, run3], [false, false, false]); expectRunsIsArchived([run1, run2, run3], [false, false, false]); }); @@ -213,9 +185,12 @@ function unarchiveSelectedRuns(): void { function isShowArchiveChanged(): void { describe('isShowArchiveChanged()', () => { describe('active runs are shown and some runs are selected', () => { - it('should unselect the runs', () => { - setRunsIsSelected([run1, run2, run3], [true, false, true]); - component.isShowArchivedChanged(); + it('should unselect the runs', async () => { + expect(await runListHarness.isShowingArchived()).toBeFalse(); + await runListHarness.clickRunListItemCheckbox(0); + await runListHarness.clickRunListItemCheckbox(2); + await runListHarness.toggleArchiveToggle(); + expect(await runListHarness.isShowingArchived()).toBeTrue(); expectRunsIsSelected([run1, run2, run3], [false, false, false]); }); }); @@ -225,47 +200,84 @@ function isShowArchiveChanged(): void { function runSelectedStatusChanged(): void { describe('runSelectedStatusChanged()', () => { describe('one run is selected', () => { - it('should show 1 run selected and indeterminate for the select all checkbox', () => { - setRunsIsSelected([run1, run2, run3], [true, false, false]); - component.runSelectedStatusChanged(); + it('should show 1 run selected and indeterminate for the select all checkbox', async () => { + await runListHarness.clickRunListItemCheckbox(0); + expect(await runListHarness.isSelectRunsCheckboxIndeterminate()).toBeTrue(); expect(component.numSelectedRuns).toEqual(1); }); }); describe('two runs are selected', () => { - it('should show 2 runs selected and indeterminate for the select all checkbox', () => { - setRunsIsSelected([run1, run2, run3], [true, true, false]); - component.runSelectedStatusChanged(); + it('should show 2 runs selected and indeterminate for the select all checkbox', async () => { + await runListHarness.clickRunListItemCheckbox(0); + await runListHarness.clickRunListItemCheckbox(1); + expect(await runListHarness.isSelectRunsCheckboxIndeterminate()).toBeTrue(); expect(component.numSelectedRuns).toEqual(2); }); }); describe('all runs are selected', () => { - it('should show 3 runs selected and checked for the select all checkbox', () => { - setRunsIsSelected([run1, run2, run3], [true, true, true]); - component.runSelectedStatusChanged(); + it('should show 3 runs selected and checked for the select all checkbox', async () => { + await runListHarness.clickRunListItemCheckbox(0); + await runListHarness.clickRunListItemCheckbox(1); + await runListHarness.clickRunListItemCheckbox(2); + expect(await runListHarness.isSelectRunsCheckboxChecked()).toBeTrue(); expect(component.numSelectedRuns).toEqual(3); }); }); }); } +function selectAllRunsCheckboxClicked(): void { + describe('selectAllRunsCheckboxClicked()', () => { + describe('select all runs checkbox is not checked', () => { + it('when select all runs checkbox is checked it should select all runs', async () => { + expectRunsIsSelected([run1, run2, run3], [false, false, false]); + expect(await runListHarness.isSelectRunsCheckboxChecked()).toBeFalse(); + await runListHarness.checkSelectRunsCheckbox(); + expectRunsIsSelected([run1, run2, run3], [true, true, true]); + expect(await runListHarness.isSelectRunsCheckboxChecked()).toBeTrue(); + }); + }); + describe('select all runs checkbox is checked', () => { + it('when select all runs checkbox is clicked it should unselect all runs', async () => { + await runListHarness.checkSelectRunsCheckbox(); + expectRunsIsSelected([run1, run2, run3], [true, true, true]); + expect(await runListHarness.isSelectRunsCheckboxChecked()).toBeTrue(); + await runListHarness.uncheckSelectRunsCheckbox(); + expectRunsIsSelected([run1, run2, run3], [false, false, false]); + expect(await runListHarness.isSelectRunsCheckboxChecked()).toBeFalse(); + }); + }); + describe('select all runs checkbox is indeterminate checked', () => { + it('when select all runs checkbox is unchecked it should unselect all runs', async () => { + await runListHarness.clickRunListItemCheckbox(0); + expectRunsIsSelected([run1, run2, run3], [false, false, true]); + expect(await runListHarness.isSelectRunsCheckboxIndeterminate()).toBeTrue(); + await runListHarness.toggleSelectRunsCheckbox(); + expectRunsIsSelected([run1, run2, run3], [false, false, false]); + expect(await runListHarness.isSelectRunsCheckboxChecked()).toBeFalse(); + }); + }); + }); +} + function selectRunsOptionChosen(): void { describe('selectRunsOptionChosen()', () => { - it('when all is chosen, it should select all runs', () => { - component.selectRunsOptionChosen('all'); + it('when all is chosen, it should select all runs2', async () => { + await runListHarness.clickSelectRunsMenuButton('All'); expectRunsIsSelected(component.filteredRuns, [true, true, true]); }); - it('when none is chosen, it should select no runs', () => { - component.selectRunsOptionChosen('none'); + it('when none is chosen, it should select no runs', async () => { + await runListHarness.clickSelectRunsMenuButton('None'); expectRunsIsSelected(component.filteredRuns, [false, false, false]); }); - it('when running is chosen, it should select running runs', () => { + it('when running is chosen, it should select running runs', async () => { setRunIsCompleted(run2); - component.selectRunsOptionChosen('running'); + await runListHarness.clickSelectRunsMenuButton('Running'); expectRunsIsSelected(component.filteredRuns, [true, false, true]); }); - it('when completed is chosen, it should select completed runs', () => { + it('when completed is chosen, it should select completed runs', async () => { setRunIsCompleted(run2); - component.selectRunsOptionChosen('completed'); + await runListHarness.clickSelectRunsMenuButton('Completed'); expectRunsIsSelected(component.filteredRuns, [false, true, false]); }); }); @@ -275,25 +287,28 @@ function setRunIsCompleted(run: TeacherRun): void { run.endTime = currentTime - 1000; } -function expectRunsIsSelected(runs: TeacherRun[], expectRunsIsSelected: boolean[]): void { - runs.forEach((run: TeacherRun, index: number) => { - expect(run.isSelected).toEqual(expectRunsIsSelected[index]); - }); -} - function runArchiveStatusChanged(): void { describe('runArchiveStatusChanged()', () => { - it('when a run is archived, it should no longer be displayed in the active view', () => { + it('when a run is archived, it should no longer be displayed in the active view', async () => { expect(!component.isShowArchived); expect(component.filteredRuns.length).toEqual(3); - setRunIsArchived(run1, true); - component.runArchiveStatusChanged(); + expect(await runListHarness.getNumRunListItems()).toEqual(3); + await runListHarness.clickRunListItemMenuButton(1, 'folderArchive'); expect(!component.isShowArchived); expect(component.filteredRuns.length).toEqual(2); + expect(await runListHarness.getNumRunListItems()).toEqual(2); }); }); } -function setRunIsArchived(run: TeacherRun, isArchived: boolean): void { - run.isArchived = isArchived; +function expectRunsIsSelected(runs: TeacherRun[], expectRunsIsSelected: boolean[]): void { + runs.forEach((run: TeacherRun, index: number) => { + expect(run.isSelected).toEqual(expectRunsIsSelected[index]); + }); +} + +function expectRunsIsArchived(runs: TeacherRun[], isArchived: boolean[]): void { + runs.forEach((run: TeacherRun, index: number) => { + expect(run.isArchived).toEqual(isArchived[index]); + }); } diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.harness.ts b/src/app/teacher/teacher-run-list/teacher-run-list.harness.ts new file mode 100644 index 00000000000..452367f07e1 --- /dev/null +++ b/src/app/teacher/teacher-run-list/teacher-run-list.harness.ts @@ -0,0 +1,86 @@ +import { ComponentHarness } from '@angular/cdk/testing'; +import { MatButtonHarness } from '@angular/material/button/testing'; +import { MatSlideToggleHarness } from '@angular/material/slide-toggle/testing'; +import { TeacherRunListItemHarness } from '../teacher-run-list-item/teacher-run-list-item.harness'; +import { SelectRunsControlsHarness } from '../select-runs-controls/select-runs-controls.harness'; + +export class TeacherRunListHarness extends ComponentHarness { + static hostSelector = 'app-teacher-run-list'; + protected getArchiveButton = this.locatorFor(MatButtonHarness.with({ text: 'Archive Selected' })); + protected getArchiveToggle = this.locatorFor(MatSlideToggleHarness); + protected getRunListItems = this.locatorForAll(TeacherRunListItemHarness); + protected getSelectRunsControls = this.locatorFor(SelectRunsControlsHarness); + protected getUnarchiveButton = this.locatorFor( + MatButtonHarness.with({ text: 'Unarchive Selected' }) + ); + + async isShowingArchived(): Promise { + const archiveToggle = await this.getArchiveToggle(); + return await archiveToggle.isChecked(); + } + + async toggleArchiveToggle(): Promise { + const archiveToggle = await this.getArchiveToggle(); + return await archiveToggle.toggle(); + } + + async checkSelectRunsCheckbox(): Promise { + const selectRunsControls = await this.getSelectRunsControls(); + return await selectRunsControls.checkCheckbox(); + } + + async uncheckSelectRunsCheckbox(): Promise { + const selectRunsControls = await this.getSelectRunsControls(); + return await selectRunsControls.uncheckCheckbox(); + } + + async toggleSelectRunsCheckbox(): Promise { + const selectRunsControls = await this.getSelectRunsControls(); + return await selectRunsControls.toggleCheckbox(); + } + + async isSelectRunsCheckboxChecked(): Promise { + const selectRunsControls = await this.getSelectRunsControls(); + return await selectRunsControls.isChecked(); + } + + async isSelectRunsCheckboxIndeterminate(): Promise { + const selectRunsControls = await this.getSelectRunsControls(); + return await selectRunsControls.isIndeterminate(); + } + + async clickSelectRunsMenuButton(menuButtonText: string): Promise { + const selectRunsControls = await this.getSelectRunsControls(); + return await selectRunsControls.clickMenuButton(menuButtonText); + } + + async clickRunListItemCheckbox(index: number): Promise { + const runListItem = await this.getRunListItem(index); + return runListItem.checkCheckbox(); + } + + async getRunListItem(index: number): Promise { + const runListItems = await this.getRunListItems(); + return runListItems[index]; + } + + async getNumRunListItems(): Promise { + const runListItems = await this.getRunListItems(); + return runListItems.length; + } + + async clickRunListItemMenuButton(index: number, menuButtonText: string): Promise { + const runListItem = await this.getRunListItem(index); + return await runListItem.clickMenuButton(menuButtonText); + } + + async clickArchiveButton(): Promise { + const button = await this.getArchiveButton(); + return await button.click(); + } + + async clickUnarchiveButton(): Promise { + const button = await this.getUnarchiveButton(); + return await button.click(); + } +} diff --git a/src/app/teacher/teacher.module.ts b/src/app/teacher/teacher.module.ts index 527aac50eff..e9903dbe281 100644 --- a/src/app/teacher/teacher.module.ts +++ b/src/app/teacher/teacher.module.ts @@ -41,7 +41,7 @@ import { ShareRunCodeDialogComponent } from './share-run-code-dialog/share-run-c import { MatToolbarModule } from '@angular/material/toolbar'; import { MatListModule } from '@angular/material/list'; import { MatSlideToggleModule } from '@angular/material/slide-toggle'; -import { SelectRunsControlsComponent } from './select-runs-controls/select-runs-controls.component'; +import { SelectRunsControlsModule } from './select-runs-controls/select-runs-controls.module'; const materialModules = [ MatAutocompleteModule, @@ -72,6 +72,7 @@ const materialModules = [ LibraryModule, materialModules, SharedModule, + SelectRunsControlsModule, TeacherRoutingModule, TimelineModule, ClipboardModule @@ -85,7 +86,6 @@ const materialModules = [ ListClassroomCoursesDialogComponent, RunMenuComponent, RunSettingsDialogComponent, - SelectRunsControlsComponent, ShareRunCodeDialogComponent, ShareRunDialogComponent, TeacherComponent, From 5efc759e04ce320742f96ff7f362eae88ac573fc Mon Sep 17 00:00:00 2001 From: Geoffrey Kwan Date: Mon, 21 Aug 2023 10:33:32 -0400 Subject: [PATCH 11/43] test(Archive Run): Clean up test harnesses #1012 --- .../select-runs-controls.harness.ts | 17 +++----- .../teacher-run-list-item.harness.ts | 8 ++-- .../teacher-run-list.harness.ts | 42 +++++++------------ 3 files changed, 23 insertions(+), 44 deletions(-) diff --git a/src/app/teacher/select-runs-controls/select-runs-controls.harness.ts b/src/app/teacher/select-runs-controls/select-runs-controls.harness.ts index 988f4ca7a50..be91ebba806 100644 --- a/src/app/teacher/select-runs-controls/select-runs-controls.harness.ts +++ b/src/app/teacher/select-runs-controls/select-runs-controls.harness.ts @@ -8,33 +8,28 @@ export class SelectRunsControlsHarness extends ComponentHarness { protected getSelectAllCheckbox = this.locatorFor(MatCheckboxHarness); async checkCheckbox(): Promise { - const selectAllCheckBox = await this.getSelectAllCheckbox(); - return await selectAllCheckBox.check(); + return (await this.getSelectAllCheckbox()).check(); } async uncheckCheckbox(): Promise { - const selectAllCheckBox = await this.getSelectAllCheckbox(); - return await selectAllCheckBox.uncheck(); + return (await this.getSelectAllCheckbox()).uncheck(); } async toggleCheckbox(): Promise { - const selectAllCheckBox = await this.getSelectAllCheckbox(); - return await selectAllCheckBox.toggle(); + return (await this.getSelectAllCheckbox()).toggle(); } async isChecked(): Promise { - const checkbox = await this.getSelectAllCheckbox(); - return await checkbox.isChecked(); + return (await this.getSelectAllCheckbox()).isChecked(); } async isIndeterminate(): Promise { - const checkbox = await this.getSelectAllCheckbox(); - return await checkbox.isIndeterminate(); + return (await this.getSelectAllCheckbox()).isIndeterminate(); } async clickMenuButton(menuButtonText: string): Promise { const menu = await this.getMenu(); await menu.open(); - return await menu.clickItem({ text: menuButtonText }); + return menu.clickItem({ text: menuButtonText }); } } diff --git a/src/app/teacher/teacher-run-list-item/teacher-run-list-item.harness.ts b/src/app/teacher/teacher-run-list-item/teacher-run-list-item.harness.ts index bd924ff8dbe..36ab707f73b 100644 --- a/src/app/teacher/teacher-run-list-item/teacher-run-list-item.harness.ts +++ b/src/app/teacher/teacher-run-list-item/teacher-run-list-item.harness.ts @@ -8,18 +8,16 @@ export class TeacherRunListItemHarness extends ComponentHarness { protected getMenu = this.locatorFor(MatMenuHarness); async checkCheckbox(): Promise { - const checkbox = await this.getCheckbox(); - return await checkbox.check(); + return (await this.getCheckbox()).check(); } async isChecked(): Promise { - const checkbox = await this.getCheckbox(); - return await checkbox.isChecked(); + return (await this.getCheckbox()).isChecked(); } async clickMenuButton(menuButtonText: string): Promise { const menu = await this.getMenu(); await menu.open(); - return await menu.clickItem({ text: menuButtonText }); + return menu.clickItem({ text: menuButtonText }); } } diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.harness.ts b/src/app/teacher/teacher-run-list/teacher-run-list.harness.ts index 452367f07e1..a634ffebe35 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.harness.ts +++ b/src/app/teacher/teacher-run-list/teacher-run-list.harness.ts @@ -15,72 +15,58 @@ export class TeacherRunListHarness extends ComponentHarness { ); async isShowingArchived(): Promise { - const archiveToggle = await this.getArchiveToggle(); - return await archiveToggle.isChecked(); + return (await this.getArchiveToggle()).isChecked(); } async toggleArchiveToggle(): Promise { - const archiveToggle = await this.getArchiveToggle(); - return await archiveToggle.toggle(); + return (await this.getArchiveToggle()).toggle(); } async checkSelectRunsCheckbox(): Promise { - const selectRunsControls = await this.getSelectRunsControls(); - return await selectRunsControls.checkCheckbox(); + return (await this.getSelectRunsControls()).checkCheckbox(); } async uncheckSelectRunsCheckbox(): Promise { - const selectRunsControls = await this.getSelectRunsControls(); - return await selectRunsControls.uncheckCheckbox(); + return (await this.getSelectRunsControls()).uncheckCheckbox(); } async toggleSelectRunsCheckbox(): Promise { - const selectRunsControls = await this.getSelectRunsControls(); - return await selectRunsControls.toggleCheckbox(); + return (await this.getSelectRunsControls()).toggleCheckbox(); } async isSelectRunsCheckboxChecked(): Promise { - const selectRunsControls = await this.getSelectRunsControls(); - return await selectRunsControls.isChecked(); + return (await this.getSelectRunsControls()).isChecked(); } async isSelectRunsCheckboxIndeterminate(): Promise { - const selectRunsControls = await this.getSelectRunsControls(); - return await selectRunsControls.isIndeterminate(); + return (await this.getSelectRunsControls()).isIndeterminate(); } async clickSelectRunsMenuButton(menuButtonText: string): Promise { - const selectRunsControls = await this.getSelectRunsControls(); - return await selectRunsControls.clickMenuButton(menuButtonText); + return (await this.getSelectRunsControls()).clickMenuButton(menuButtonText); } async clickRunListItemCheckbox(index: number): Promise { - const runListItem = await this.getRunListItem(index); - return runListItem.checkCheckbox(); + return (await this.getRunListItem(index)).checkCheckbox(); } async getRunListItem(index: number): Promise { - const runListItems = await this.getRunListItems(); - return runListItems[index]; + return (await this.getRunListItems())[index]; } async getNumRunListItems(): Promise { - const runListItems = await this.getRunListItems(); - return runListItems.length; + return (await this.getRunListItems()).length; } async clickRunListItemMenuButton(index: number, menuButtonText: string): Promise { - const runListItem = await this.getRunListItem(index); - return await runListItem.clickMenuButton(menuButtonText); + return (await this.getRunListItem(index)).clickMenuButton(menuButtonText); } async clickArchiveButton(): Promise { - const button = await this.getArchiveButton(); - return await button.click(); + return (await this.getArchiveButton()).click(); } async clickUnarchiveButton(): Promise { - const button = await this.getUnarchiveButton(); - return await button.click(); + return (await this.getUnarchiveButton()).click(); } } From c8c0463ea1382ca88cb2b6247054a9ea194e11f1 Mon Sep 17 00:00:00 2001 From: Geoffrey Kwan Date: Mon, 21 Aug 2023 12:38:45 -0400 Subject: [PATCH 12/43] feat(Archive Run): Change http requests to use new methods and paths #1012 --- src/app/teacher/teacher.service.ts | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/app/teacher/teacher.service.ts b/src/app/teacher/teacher.service.ts index f83ee79c99f..cbb0e332e72 100644 --- a/src/app/teacher/teacher.service.ts +++ b/src/app/teacher/teacher.service.ts @@ -65,30 +65,27 @@ export class TeacherService { return this.http.get(`${this.lastRunUrl}/${projectId}`); } - archiveRun(run: Run): Observable { - const params = new HttpParams().set('projectId', run.project.id); - return this.http.post(`/api/archive/project`, params); + archiveRun(run: Run): Observable { + return this.http.put(`/api/project/${run.project.id}/archived`, null); } - archiveRuns(runs: Run[]): Observable { - let params = new HttpParams(); - for (const run of runs) { - params = params.append('projectIds', run.project.id); - } - return this.http.post(`/api/archive/projects`, params); + archiveRuns(runs: Run[]): Observable { + const projectIds = runs.map((run) => run.project.id); + return this.http.put(`/api/projects/archived`, projectIds); } - unarchiveRun(run: Run): Observable { - const params = new HttpParams().set('projectId', run.project.id); - return this.http.post(`/api/unarchive/project`, params); + unarchiveRun(run: Run): Observable { + return this.http.delete(`/api/project/${run.project.id}/archived`); } - unarchiveRuns(runs: Run[]): Observable { + unarchiveRuns(runs: Run[]): Observable { let params = new HttpParams(); for (const run of runs) { params = params.append('projectIds', run.project.id); } - return this.http.post(`/api/unarchive/projects`, params); + return this.http.delete(`/api/projects/archived`, { + params: params + }); } registerTeacherAccount(teacherUser: Teacher): Observable { From 28a971e3931e29b9ca0070e38887610a2b83127f Mon Sep 17 00:00:00 2001 From: Geoffrey Kwan Date: Mon, 21 Aug 2023 14:40:28 -0400 Subject: [PATCH 13/43] chore(Archive Run): Create ArchiveProjectService and move functions in #1012 --- src/app/app.module.ts | 8 +++-- src/app/domain/project.ts | 1 + src/app/services/archive-project.service.ts | 32 +++++++++++++++++++ .../run-menu/run-menu.component.spec.ts | 28 ++++++++++++---- .../teacher/run-menu/run-menu.component.ts | 10 +++--- .../teacher-run-list.component.spec.ts | 30 +++++++++-------- .../teacher-run-list.component.ts | 11 +++++-- src/app/teacher/teacher.service.ts | 23 ------------- 8 files changed, 90 insertions(+), 53 deletions(-) create mode 100644 src/app/services/archive-project.service.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index de746534798..347fd654226 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -23,6 +23,7 @@ import { AnnouncementComponent } from './announcement/announcement.component'; import { AnnouncementDialogComponent } from './announcement/announcement.component'; import { TrackScrollDirective } from './track-scroll.directive'; import { RecaptchaV3Module, RECAPTCHA_V3_SITE_KEY, RECAPTCHA_BASE_URL } from 'ng-recaptcha'; +import { ArchiveProjectService } from './services/archive-project.service'; export function initialize( configService: ConfigService, @@ -59,11 +60,12 @@ export function initialize( MatDialogModule, RecaptchaV3Module, RouterModule.forRoot([], { - scrollPositionRestoration: 'enabled', - anchorScrolling: 'enabled' -}) + scrollPositionRestoration: 'enabled', + anchorScrolling: 'enabled' + }) ], providers: [ + ArchiveProjectService, ConfigService, StudentService, TeacherService, diff --git a/src/app/domain/project.ts b/src/app/domain/project.ts index c44cd5130fd..d25e4cc063c 100644 --- a/src/app/domain/project.ts +++ b/src/app/domain/project.ts @@ -2,6 +2,7 @@ import { Run } from './run'; import { User } from '../domain/user'; export class Project { + archived: boolean; dateArchived: string; dateCreated: string; id: number; diff --git a/src/app/services/archive-project.service.ts b/src/app/services/archive-project.service.ts new file mode 100644 index 00000000000..2ae7ddb8be7 --- /dev/null +++ b/src/app/services/archive-project.service.ts @@ -0,0 +1,32 @@ +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { Project } from '../domain/project'; + +@Injectable() +export class ArchiveProjectService { + constructor(private http: HttpClient) {} + + archiveProject(project: Project): Observable { + return this.http.put(`/api/project/${project.id}/archived`, null); + } + + archiveProjects(projects: Project[]): Observable { + const projectIds = projects.map((project) => project.id); + return this.http.put(`/api/projects/archived`, projectIds); + } + + unarchiveProject(project: Project): Observable { + return this.http.delete(`/api/project/${project.id}/archived`); + } + + unarchiveProjects(projects: Project[]): Observable { + let params = new HttpParams(); + for (const project of projects) { + params = params.append('projectIds', project.id); + } + return this.http.delete(`/api/projects/archived`, { + params: params + }); + } +} diff --git a/src/app/teacher/run-menu/run-menu.component.spec.ts b/src/app/teacher/run-menu/run-menu.component.spec.ts index c1f4e542346..5cdcda0e7f3 100644 --- a/src/app/teacher/run-menu/run-menu.component.spec.ts +++ b/src/app/teacher/run-menu/run-menu.component.spec.ts @@ -13,6 +13,9 @@ import { Course } from '../../domain/course'; import { RouterTestingModule } from '@angular/router/testing'; import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { ArchiveProjectService } from '../../services/archive-project.service'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { Project } from '../../domain/project'; export class MockTeacherService { checkClassroomAuthorization(): Observable { @@ -27,12 +30,6 @@ export class MockTeacherService { observer.complete(); }); } - archiveRun(run: TeacherRun) { - return of({}); - } - unarchiveRun(run: TeacherRun) { - return of({}); - } } export class MockUserService { @@ -63,6 +60,7 @@ export class MockConfigService { } } +let archiveProjectService: ArchiveProjectService; let component: RunMenuComponent; let fixture: ComponentFixture; let snackBarSpy: jasmine.Spy; @@ -72,9 +70,16 @@ describe('RunMenuComponent', () => { beforeEach( waitForAsync(() => { TestBed.configureTestingModule({ - imports: [BrowserAnimationsModule, MatMenuModule, MatSnackBarModule, RouterTestingModule], + imports: [ + BrowserAnimationsModule, + HttpClientTestingModule, + MatMenuModule, + MatSnackBarModule, + RouterTestingModule + ], declarations: [RunMenuComponent], providers: [ + ArchiveProjectService, { provide: TeacherService, useClass: MockTeacherService }, { provide: UserService, useClass: MockUserService }, { provide: ConfigService, useClass: MockConfigService }, @@ -99,8 +104,17 @@ describe('RunMenuComponent', () => { sharedOwners: [] } }); + archiveProjectService = TestBed.inject(ArchiveProjectService); teacherService = TestBed.inject(TeacherService); snackBarSpy = spyOn(TestBed.inject(MatSnackBar), 'open'); + spyOn(archiveProjectService, 'archiveProject').and.callFake((project: Project) => { + project.archived = true; + return of(project); + }); + spyOn(archiveProjectService, 'unarchiveProject').and.callFake((project: Project) => { + project.archived = false; + return of(project); + }); fixture.detectChanges(); }); diff --git a/src/app/teacher/run-menu/run-menu.component.ts b/src/app/teacher/run-menu/run-menu.component.ts index 97500eede7d..3c879b2fd6c 100644 --- a/src/app/teacher/run-menu/run-menu.component.ts +++ b/src/app/teacher/run-menu/run-menu.component.ts @@ -8,8 +8,8 @@ import { ConfigService } from '../../services/config.service'; import { RunSettingsDialogComponent } from '../run-settings-dialog/run-settings-dialog.component'; import { EditRunWarningDialogComponent } from '../edit-run-warning-dialog/edit-run-warning-dialog.component'; import { Router } from '@angular/router'; -import { TeacherService } from '../teacher.service'; import { MatSnackBar } from '@angular/material/snack-bar'; +import { ArchiveProjectService } from '../../services/archive-project.service'; @Component({ selector: 'app-run-menu', @@ -25,12 +25,12 @@ export class RunMenuComponent implements OnInit { reportProblemLink: string = ''; constructor( + private archiveProjectService: ArchiveProjectService, private dialog: MatDialog, private userService: UserService, private configService: ConfigService, private router: Router, - private snackBar: MatSnackBar, - private teacherService: TeacherService + private snackBar: MatSnackBar ) {} ngOnInit() { @@ -92,7 +92,7 @@ export class RunMenuComponent implements OnInit { archive(): void { const run = this.run; - this.teacherService.archiveRun(run).subscribe({ + this.archiveProjectService.archiveProject(run.project).subscribe({ next: () => { run.isArchived = true; this.runArchiveStatusChangedEvent.emit(run); @@ -106,7 +106,7 @@ export class RunMenuComponent implements OnInit { unarchive(): void { const run = this.run; - this.teacherService.unarchiveRun(run).subscribe({ + this.archiveProjectService.unarchiveProject(run.project).subscribe({ next: () => { run.isArchived = false; this.runArchiveStatusChangedEvent.emit(run); diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts b/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts index 947f05b0ef7..8543f50f13f 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts @@ -24,9 +24,12 @@ import { MatFormFieldModule } from '@angular/material/form-field'; import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { RunMenuComponent } from '../run-menu/run-menu.component'; import { MatMenuModule } from '@angular/material/menu'; +import { ArchiveProjectService } from '../../services/archive-project.service'; +import { Project } from '../../domain/project'; class TeacherScheduleStubComponent {} +let archiveProjectService: ArchiveProjectService; let component: TeacherRunListComponent; let configService: ConfigService; const currentTime = new Date().getTime(); @@ -77,13 +80,14 @@ describe('TeacherRunListComponent', () => { ]), SelectRunsControlsModule ], - providers: [TeacherService, ConfigService, UserService], + providers: [ArchiveProjectService, ConfigService, TeacherService, UserService], schemas: [NO_ERRORS_SCHEMA] }); }) ); beforeEach(async () => { + archiveProjectService = TestBed.inject(ArchiveProjectService); configService = TestBed.inject(ConfigService); teacherService = TestBed.inject(TeacherService); userService = TestBed.inject(UserService); @@ -97,21 +101,21 @@ describe('TeacherRunListComponent', () => { spyOn(configService, 'getCurrentServerTime').and.returnValue(currentTime); spyOn(configService, 'getContextPath').and.returnValue(''); spyOn(userService, 'getUserId').and.returnValue(1); - spyOn(teacherService, 'archiveRun').and.callFake((run: TeacherRun) => { - run.isArchived = true; - return of(run); + spyOn(archiveProjectService, 'archiveProject').and.callFake((project: Project) => { + project.archived = true; + return of(project); }); - spyOn(teacherService, 'archiveRuns').and.callFake((runs: TeacherRun[]) => { - runs.forEach((run) => (run.isArchived = true)); - return of(runs); + spyOn(archiveProjectService, 'archiveProjects').and.callFake((projects: Project[]) => { + projects.forEach((project) => (project.archived = true)); + return of(projects); }); - spyOn(teacherService, 'unarchiveRun').and.callFake((run: TeacherRun) => { - run.isArchived = false; - return of(run); + spyOn(archiveProjectService, 'unarchiveProject').and.callFake((project: Project) => { + project.archived = false; + return of(project); }); - spyOn(teacherService, 'unarchiveRuns').and.callFake((runs: TeacherRun[]) => { - runs.forEach((run) => (run.isArchived = false)); - return of(runs); + spyOn(archiveProjectService, 'unarchiveProjects').and.callFake((projects: Project[]) => { + projects.forEach((project) => (project.archived = false)); + return of(projects); }); fixture = TestBed.createComponent(TeacherRunListComponent); component = fixture.componentInstance; diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.ts b/src/app/teacher/teacher-run-list/teacher-run-list.component.ts index a8fe13c8d7c..6d7b80d23c5 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.ts +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.ts @@ -8,6 +8,8 @@ import { Observable, of, Subscription } from 'rxjs'; import { UserService } from '../../services/user.service'; import { mergeMap } from 'rxjs/operators'; import { MatSnackBar } from '@angular/material/snack-bar'; +import { ArchiveProjectService } from '../../services/archive-project.service'; +import { Project } from '../../domain/project'; @Component({ selector: 'app-teacher-run-list', @@ -29,6 +31,7 @@ export class TeacherRunListComponent implements OnInit { subscriptions: Subscription = new Subscription(); constructor( + private archiveProjectService: ArchiveProjectService, private configService: ConfigService, @Inject(LOCALE_ID) private localeID: string, private route: ActivatedRoute, @@ -233,7 +236,7 @@ export class TeacherRunListComponent implements OnInit { protected archiveSelectedRuns(): Subscription { const runs = this.getSelectedRuns(); - return this.teacherService.archiveRuns(runs).subscribe({ + return this.archiveProjectService.archiveProjects(this.getProjects(runs)).subscribe({ next: () => { this.setRunsIsArchived(runs, true); this.unselectAllRuns(); @@ -249,7 +252,7 @@ export class TeacherRunListComponent implements OnInit { protected unarchiveSelectedRuns(): Subscription { const runs = this.getSelectedRuns(); - return this.teacherService.unarchiveRuns(runs).subscribe({ + return this.archiveProjectService.unarchiveProjects(this.getProjects(runs)).subscribe({ next: () => { this.setRunsIsArchived(runs, false); this.unselectAllRuns(); @@ -263,6 +266,10 @@ export class TeacherRunListComponent implements OnInit { }); } + private getProjects(runs: TeacherRun[]): Project[] { + return runs.map((run: TeacherRun) => run.project); + } + private setRunsIsArchived(runs: TeacherRun[], isArchived: boolean): void { for (const run of runs) { run.isArchived = isArchived; diff --git a/src/app/teacher/teacher.service.ts b/src/app/teacher/teacher.service.ts index cbb0e332e72..0bcada405e6 100644 --- a/src/app/teacher/teacher.service.ts +++ b/src/app/teacher/teacher.service.ts @@ -65,29 +65,6 @@ export class TeacherService { return this.http.get(`${this.lastRunUrl}/${projectId}`); } - archiveRun(run: Run): Observable { - return this.http.put(`/api/project/${run.project.id}/archived`, null); - } - - archiveRuns(runs: Run[]): Observable { - const projectIds = runs.map((run) => run.project.id); - return this.http.put(`/api/projects/archived`, projectIds); - } - - unarchiveRun(run: Run): Observable { - return this.http.delete(`/api/project/${run.project.id}/archived`); - } - - unarchiveRuns(runs: Run[]): Observable { - let params = new HttpParams(); - for (const run of runs) { - params = params.append('projectIds', run.project.id); - } - return this.http.delete(`/api/projects/archived`, { - params: params - }); - } - registerTeacherAccount(teacherUser: Teacher): Observable { const headers = { 'Content-Type': 'application/json' From 9fbbd4873d83ba7dc209464256f4684c886d6c61 Mon Sep 17 00:00:00 2001 From: Geoffrey Kwan Date: Tue, 22 Aug 2023 10:39:15 -0400 Subject: [PATCH 14/43] feat(Archive Run): Archive requests now look at project status in response #1012 --- src/app/domain/archiveProjectResponse.ts | 4 ++ .../teacher/run-menu/run-menu.component.html | 4 +- .../run-menu/run-menu.component.spec.ts | 4 +- .../teacher/run-menu/run-menu.component.ts | 9 +-- .../teacher-run-list-item.component.html | 4 +- .../teacher-run-list-item.component.spec.ts | 2 +- .../teacher-run-list-item.component.ts | 8 +-- .../teacher-run-list.component.html | 10 +-- .../teacher-run-list.component.spec.ts | 18 ++--- .../teacher-run-list.component.ts | 71 +++++++++++-------- src/app/teacher/teacher-run.ts | 6 +- 11 files changed, 80 insertions(+), 60 deletions(-) create mode 100644 src/app/domain/archiveProjectResponse.ts diff --git a/src/app/domain/archiveProjectResponse.ts b/src/app/domain/archiveProjectResponse.ts new file mode 100644 index 00000000000..fb33f0a6ce7 --- /dev/null +++ b/src/app/domain/archiveProjectResponse.ts @@ -0,0 +1,4 @@ +export class ArchiveProjectResponse { + archived: boolean; + id: number; +} diff --git a/src/app/teacher/run-menu/run-menu.component.html b/src/app/teacher/run-menu/run-menu.component.html index 8b907f0f8cc..109e9f29883 100644 --- a/src/app/teacher/run-menu/run-menu.component.html +++ b/src/app/teacher/run-menu/run-menu.component.html @@ -29,11 +29,11 @@ report_problem Report Problem - + folder Archive - + folder_off Unarchive diff --git a/src/app/teacher/run-menu/run-menu.component.spec.ts b/src/app/teacher/run-menu/run-menu.component.spec.ts index 5cdcda0e7f3..b3cecea6beb 100644 --- a/src/app/teacher/run-menu/run-menu.component.spec.ts +++ b/src/app/teacher/run-menu/run-menu.component.spec.ts @@ -126,7 +126,7 @@ function archive() { describe('archive()', () => { it('should archive a run', () => { component.archive(); - expect(component.run.isArchived).toEqual(true); + expect(component.run.archived).toEqual(true); expect(snackBarSpy).toHaveBeenCalledWith('Successfully Archived Run'); }); }); @@ -136,7 +136,7 @@ function unarchive() { describe('unarchive()', () => { it('should unarchive a run', () => { component.unarchive(); - expect(component.run.isArchived).toEqual(false); + expect(component.run.archived).toEqual(false); expect(snackBarSpy).toHaveBeenCalledWith('Successfully Unarchived Run'); }); }); diff --git a/src/app/teacher/run-menu/run-menu.component.ts b/src/app/teacher/run-menu/run-menu.component.ts index 3c879b2fd6c..cd93275d61a 100644 --- a/src/app/teacher/run-menu/run-menu.component.ts +++ b/src/app/teacher/run-menu/run-menu.component.ts @@ -10,6 +10,7 @@ import { EditRunWarningDialogComponent } from '../edit-run-warning-dialog/edit-r import { Router } from '@angular/router'; import { MatSnackBar } from '@angular/material/snack-bar'; import { ArchiveProjectService } from '../../services/archive-project.service'; +import { ArchiveProjectResponse } from '../../domain/archiveProjectResponse'; @Component({ selector: 'app-run-menu', @@ -93,8 +94,8 @@ export class RunMenuComponent implements OnInit { archive(): void { const run = this.run; this.archiveProjectService.archiveProject(run.project).subscribe({ - next: () => { - run.isArchived = true; + next: (response: ArchiveProjectResponse) => { + run.archived = response.archived; this.runArchiveStatusChangedEvent.emit(run); this.snackBar.open($localize`Successfully Archived Run`); }, @@ -107,8 +108,8 @@ export class RunMenuComponent implements OnInit { unarchive(): void { const run = this.run; this.archiveProjectService.unarchiveProject(run.project).subscribe({ - next: () => { - run.isArchived = false; + next: (response: ArchiveProjectResponse) => { + run.archived = response.archived; this.runArchiveStatusChangedEvent.emit(run); this.snackBar.open($localize`Successfully Unarchived Run`); }, diff --git a/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html b/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html index 343539ebc79..c1e31624bce 100644 --- a/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html +++ b/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html @@ -53,13 +53,13 @@
{ - this.run.isHighlighted = false; + this.run.highlighted = false; }, 7000); } } ngAfterViewInit() { - if (this.run.isHighlighted) { + if (this.run.highlighted) { this.elRef.nativeElement.querySelector('mat-card').scrollIntoView(); } } @@ -106,7 +106,7 @@ export class TeacherRunListItemComponent implements OnInit { } runArchiveStatusChanged(): void { - this.run.isSelected = false; + this.run.selected = false; this.runSelectedStatusChangedEvent.emit(); this.runArchiveStatusChangedEvent.emit(); } diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.html b/src/app/teacher/teacher-run-list/teacher-run-list.component.html index cc28623b54e..e025f2730ff 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.html +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.html @@ -24,7 +24,7 @@
Active Archived @@ -36,10 +36,10 @@ >Units found: {{ filteredRuns.length }} Total {{ isShowArchived ? 'archived' : 'active' }} classroom units: + >Total {{ showArchived ? 'archived' : 'active' }} classroom units: {{ filteredRuns.length }} - + ({{ completedTotal() }} completed, @@ -76,7 +76,7 @@

From cd6a1a05f1a0942ff3301d7c7640ce76c11751ec Mon Sep 17 00:00:00 2001 From: Jonathan Lim-Breitbart Date: Thu, 24 Aug 2023 15:55:00 -0700 Subject: [PATCH 25/43] Update run selection tools - Replace archive/restore action button with icon button - Update some text for brevity/clarity - Add aria-labels and tooltips - Clean up layout styles --- .../select-runs-controls.component.html | 11 +- .../select-runs-controls.component.scss | 10 +- .../select-runs-controls.module.ts | 10 +- .../teacher-run-list.component.html | 47 ++-- .../teacher-run-list.component.scss | 11 - .../teacher-run-list.component.ts | 5 + src/messages.xlf | 244 +++++++++--------- 7 files changed, 181 insertions(+), 157 deletions(-) diff --git a/src/app/teacher/select-runs-controls/select-runs-controls.component.html b/src/app/teacher/select-runs-controls/select-runs-controls.component.html index ebc0ed06a5c..a49e1dc68c0 100644 --- a/src/app/teacher/select-runs-controls/select-runs-controls.component.html +++ b/src/app/teacher/select-runs-controls/select-runs-controls.component.html @@ -1,13 +1,20 @@

- diff --git a/src/app/teacher/select-runs-controls/select-runs-controls.component.scss b/src/app/teacher/select-runs-controls/select-runs-controls.component.scss index c99549f0ed1..91b72c5ce44 100644 --- a/src/app/teacher/select-runs-controls/select-runs-controls.component.scss +++ b/src/app/teacher/select-runs-controls/select-runs-controls.component.scss @@ -1,11 +1,3 @@ -.select-all-checkbox { - margin-right: 6px; -} - .select-all-drop-down { - margin-right: 24px; + margin: 0 -8px; } - -.mat-icon { - margin: 0px; -} \ No newline at end of file diff --git a/src/app/teacher/select-runs-controls/select-runs-controls.module.ts b/src/app/teacher/select-runs-controls/select-runs-controls.module.ts index ed3b56eeb20..05bbe6425de 100644 --- a/src/app/teacher/select-runs-controls/select-runs-controls.module.ts +++ b/src/app/teacher/select-runs-controls/select-runs-controls.module.ts @@ -5,10 +5,18 @@ import { MatMenuModule } from '@angular/material/menu'; import { SelectRunsControlsComponent } from './select-runs-controls.component'; import { FlexLayoutModule } from '@angular/flex-layout'; import { MatButtonModule } from '@angular/material/button'; +import { MatTooltipModule } from '@angular/material/tooltip'; @NgModule({ declarations: [SelectRunsControlsComponent], exports: [SelectRunsControlsComponent], - imports: [FlexLayoutModule, MatButtonModule, MatCheckboxModule, MatIconModule, MatMenuModule] + imports: [ + FlexLayoutModule, + MatButtonModule, + MatCheckboxModule, + MatIconModule, + MatMenuModule, + MatTooltipModule + ] }) export class SelectRunsControlsModule {} diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.html b/src/app/teacher/teacher-run-list/teacher-run-list.component.html index 6cca7773a06..b99d3d48e71 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.html +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.html @@ -1,6 +1,6 @@

Hey there! Looks like you haven't run any WISE units in your classes yet.

-

Select "Browse Units" to find titles to use with your students.

+

Browse the "Unit Library" to find titles to use with your students.

-
+

Units found: {{ filteredRuns.length }} - Total {{ showArchived ? 'archived' : 'active' }} classroom units: - {{ filteredRuns.length }} + + Archived classroom units: + Active classroom units: + {{ filteredRuns.length }} + ({{ completedTotal() }} completed, @@ -51,7 +52,12 @@ | - Clear filters @@ -62,33 +68,38 @@ class="select-all-controls" fxLayout="row" fxLayoutAlign="start center" - fxLayoutGap="20px" + fxLayoutGap="8px" > -

- {{ numSelectedRuns }} Run(s) Selected +
+ {{ numSelectedRuns }} selected
diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.scss b/src/app/teacher/teacher-run-list/teacher-run-list.component.scss index 3c20207ab36..d893cc68870 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.scss +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.scss @@ -23,14 +23,3 @@ h2 { .notice { font-weight: 500; } - -.select-all-controls { - margin-left: 38px; - height: 36px; -} - -.num-selected-runs-display { - font-weight: 500; - font-size: 14px; - margin-right: 24px; -} diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.ts b/src/app/teacher/teacher-run-list/teacher-run-list.component.ts index 84421411e43..d7433583ced 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.ts +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.ts @@ -164,6 +164,11 @@ export class TeacherRunListComponent implements OnInit { ); } + protected clearFilters(event: Event): void { + event.preventDefault(); + this.reset(); + } + protected reset(): void { this.searchValue = ''; this.filterValue = ''; diff --git a/src/messages.xlf b/src/messages.xlf index 8fb6f28e3b7..204ff717a7b 100644 --- a/src/messages.xlf +++ b/src/messages.xlf @@ -5480,7 +5480,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html - 79 + 88 @@ -6182,7 +6182,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/app/teacher/teacher-run-list/teacher-run-list.component.html - 128 + 125 @@ -6300,6 +6300,10 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.src/app/notebook/notebook-report/notebook-report.component.html 42 + + src/app/teacher/run-menu/run-menu.component.html + 38 + Save @@ -7543,7 +7547,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html - 112 + 124 @@ -7582,7 +7586,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/app/teacher/teacher-run-list/teacher-run-list.component.html - 36 + 35 @@ -8226,54 +8230,51 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.src/app/teacher/run-menu/run-menu.component.html 34 - - - Unarchive - src/app/teacher/run-menu/run-menu.component.html - 38 + src/app/teacher/teacher-run-list/teacher-run-list.component.html + 87 Run Settings src/app/teacher/run-menu/run-menu.component.ts - 74 + 75 Edit Classroom Unit Warning src/app/teacher/run-menu/run-menu.component.ts - 84 + 85 - - Successfully Archived Run + + Successfully archived unit. src/app/teacher/run-menu/run-menu.component.ts - 99 + 100 - - Error Archiving Run + + Error archiving unit. src/app/teacher/run-menu/run-menu.component.ts - 102 + 103 - - Successfully Unarchived Run + + Successfully restored unit. src/app/teacher/run-menu/run-menu.component.ts - 113 + 114 - - Error Unarchiving Run + + Error restoring unit. src/app/teacher/run-menu/run-menu.component.ts - 116 + 117 @@ -8479,6 +8480,76 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.305 + + Select + + src/app/teacher/select-runs-controls/select-runs-controls.component.html + 7 + + + src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html + 64 + + + + Select units + + src/app/teacher/select-runs-controls/select-runs-controls.component.html + 15 + + + + All + + src/app/teacher/select-runs-controls/select-runs-controls.component.html + 21 + + + src/assets/wise5/authoringTool/milestones-authoring/milestones-authoring.component.html + 114 + + + src/assets/wise5/authoringTool/milestones-authoring/milestones-authoring.component.html + 367 + + + src/assets/wise5/classroomMonitor/dataExport/data-export/data-export.component.html + 458 + + + + None + + src/app/teacher/select-runs-controls/select-runs-controls.component.html + 22 + + + src/assets/wise5/authoringTool/peer-grouping/select-peer-grouping-option/select-peer-grouping-option.component.html + 18 + + + + Running + + src/app/teacher/select-runs-controls/select-runs-controls.component.html + 23 + + + + Completed + + src/app/teacher/select-runs-controls/select-runs-controls.component.html + 24 + + + src/assets/wise5/classroomMonitor/classroomMonitorComponents/milestones/milestone-details/milestone-details.component.html + 85 + + + src/assets/wise5/themes/default/themeComponents/nodeStatusIcon/node-status-icon.component.html + 20 + + Share with Students @@ -8722,21 +8793,21 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.(Legacy Unit) src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html - 82 + 91 Last student login: src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html - 109 + 121 Teacher Tools src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html - 120 + 132 @@ -8753,32 +8824,25 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.2 - - Select "Browse Units" to find titles to use with your students. + + Browse the "Unit Library" to find titles to use with your students. src/app/teacher/teacher-run-list/teacher-run-list.component.html 3 - - Active - - src/app/teacher/teacher-run-list/teacher-run-list.component.html - 25 - - - - Archived + + Archived classroom units: src/app/teacher/teacher-run-list/teacher-run-list.component.html - 30 + 38 - - Total classroom units: + + Active classroom units: src/app/teacher/teacher-run-list/teacher-run-list.component.html - 39,40 + 39 @@ -8799,108 +8863,56 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.Clear filters src/app/teacher/teacher-run-list/teacher-run-list.component.html - 56 - - - - All - - src/app/teacher/teacher-run-list/teacher-run-list.component.html - 79 - - - src/assets/wise5/authoringTool/milestones-authoring/milestones-authoring.component.html - 114 - - - src/assets/wise5/authoringTool/milestones-authoring/milestones-authoring.component.html - 367 - - - src/assets/wise5/classroomMonitor/dataExport/data-export/data-export.component.html - 458 - - - - None - - src/app/teacher/teacher-run-list/teacher-run-list.component.html - 80 - - - src/assets/wise5/authoringTool/peer-grouping/select-peer-grouping-option/select-peer-grouping-option.component.html - 18 + 61 - - Running + + selected src/app/teacher/teacher-run-list/teacher-run-list.component.html - 81 + 78,80 - - Completed + + Archive selected units src/app/teacher/teacher-run-list/teacher-run-list.component.html - 82 - - - src/assets/wise5/classroomMonitor/classroomMonitorComponents/milestones/milestone-details/milestone-details.component.html 85 - - src/assets/wise5/themes/default/themeComponents/nodeStatusIcon/node-status-icon.component.html - 20 - - - - Run(s) Selected - - src/app/teacher/teacher-run-list/teacher-run-list.component.html - 85,87 - - - Archive Selected + + Restore selected units src/app/teacher/teacher-run-list/teacher-run-list.component.html - 94,96 - - - - Unarchive Selected - - src/app/teacher/teacher-run-list/teacher-run-list.component.html - 103,105 + 97 - - Successfully Archived Runs + + Successfully archived unit(s). src/app/teacher/teacher-run-list/teacher-run-list.component.ts - 285 + 250,252 - - Error Archiving Runs + + Error archiving unit(s). src/app/teacher/teacher-run-list/teacher-run-list.component.ts - 288 + 256 - - Successfully Unarchived Runs + + Successfully restored unit(s). src/app/teacher/teacher-run-list/teacher-run-list.component.ts - 301 + 268,270 - - Error Unarchiving Runs + + Error restoring unit(s). src/app/teacher/teacher-run-list/teacher-run-list.component.ts - 304 + 274 @@ -11719,8 +11731,8 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.63 - - Please Try Again (Error: Duplicate Tag) + + Please try again (Error: duplicate tag). src/assets/wise5/authoringTool/peer-grouping/create-new-peer-grouping-dialog/create-new-peer-grouping-dialog.component.ts 37 From 9053114a7680409de6f5ca4c19de2e87a8ebb9d5 Mon Sep 17 00:00:00 2001 From: Jonathan Lim-Breitbart Date: Fri, 25 Aug 2023 00:00:57 -0700 Subject: [PATCH 26/43] Fix tests, update some text and function names --- .../teacher/run-menu/run-menu.component.spec.ts | 4 ++-- src/app/teacher/run-menu/run-menu.harness.ts | 4 ++-- .../teacher-run-list.component.html | 6 +++--- .../teacher-run-list.component.spec.ts | 16 ++++++++-------- .../teacher-run-list.component.ts | 2 +- .../teacher-run-list/teacher-run-list.harness.ts | 16 +++++++++------- .../create-new-peer-grouping-dialog.component.ts | 2 +- src/messages.xlf | 14 ++++++++++++++ 8 files changed, 40 insertions(+), 24 deletions(-) diff --git a/src/app/teacher/run-menu/run-menu.component.spec.ts b/src/app/teacher/run-menu/run-menu.component.spec.ts index 704236e7ab4..bdef07f28fd 100644 --- a/src/app/teacher/run-menu/run-menu.component.spec.ts +++ b/src/app/teacher/run-menu/run-menu.component.spec.ts @@ -132,7 +132,7 @@ function archive() { it('should archive a run', async () => { await runMenuHarness.clickArchiveMenuButton(); expect(component.run.archived).toEqual(true); - expect(snackBarSpy).toHaveBeenCalledWith('Successfully Archived Unit'); + expect(snackBarSpy).toHaveBeenCalledWith('Successfully archived unit.'); }); }); } @@ -144,7 +144,7 @@ function unarchive() { component.ngOnInit(); await runMenuHarness.clickUnarchiveMenuButton(); expect(component.run.archived).toEqual(false); - expect(snackBarSpy).toHaveBeenCalledWith('Successfully Unarchived Unit'); + expect(snackBarSpy).toHaveBeenCalledWith('Successfully restored unit.'); }); }); } diff --git a/src/app/teacher/run-menu/run-menu.harness.ts b/src/app/teacher/run-menu/run-menu.harness.ts index 74f0a03ddab..2f4b8723460 100644 --- a/src/app/teacher/run-menu/run-menu.harness.ts +++ b/src/app/teacher/run-menu/run-menu.harness.ts @@ -12,10 +12,10 @@ export class RunMenuHarness extends ComponentHarness { } async clickArchiveMenuButton(): Promise { - return await this.clickMenuButton('folderArchive'); + return await this.clickMenuButton('archiveArchive'); } async clickUnarchiveMenuButton(): Promise { - return await this.clickMenuButton('folder_offUnarchive'); + return await this.clickMenuButton('unarchiveRestore'); } } diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.html b/src/app/teacher/teacher-run-list/teacher-run-list.component.html index b99d3d48e71..1705a382d19 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.html +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.html @@ -23,9 +23,9 @@ View - - Active - Archived + + Active + Archived
diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts b/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts index 20b856b3392..c3c5554b6fc 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts @@ -21,12 +21,12 @@ import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatInputModule } from '@angular/material/input'; import { FormsModule } from '@angular/forms'; import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { RunMenuComponent } from '../run-menu/run-menu.component'; import { MatMenuModule } from '@angular/material/menu'; import { ArchiveProjectService } from '../../services/archive-project.service'; import { MatCardModule } from '@angular/material/card'; import { MockArchiveProjectService } from '../../services/mock-archive-project.service'; +import { MatSelectModule } from '@angular/material/select'; class TeacherScheduleStubComponent {} @@ -88,7 +88,7 @@ describe('TeacherRunListComponent', () => { MatFormFieldModule, MatInputModule, MatMenuModule, - MatSlideToggleModule, + MatSelectModule, MatSnackBarModule, RouterTestingModule.withRoutes([ { path: 'teacher/home/schedule', component: TeacherScheduleStubComponent } @@ -131,7 +131,7 @@ describe('TeacherRunListComponent', () => { }); archiveSelectedRuns(); - isShowArchiveChanged(); + showArchivedChanged(); runArchiveStatusChanged(); runSelectedStatusChanged(); selectAllRunsCheckboxClicked(); @@ -171,7 +171,7 @@ function unarchiveSelectedRuns(): void { ]) ); component.ngOnInit(); - await runListHarness.toggleArchiveToggle(); + await runListHarness.showArchived(); expect(await runListHarness.getNumRunListItems()).toEqual(2); await runListHarness.clickRunListItemCheckbox(0); await runListHarness.clickRunListItemCheckbox(1); @@ -181,14 +181,14 @@ function unarchiveSelectedRuns(): void { }); } -function isShowArchiveChanged(): void { - describe('isShowArchiveChanged()', () => { +function showArchivedChanged(): void { + describe('showArchivedChanged()', () => { describe('active runs are shown and some runs are selected', () => { it('should unselect the runs', async () => { expect(await runListHarness.isShowingArchived()).toBeFalse(); await runListHarness.clickRunListItemCheckbox(0); await runListHarness.clickRunListItemCheckbox(2); - await runListHarness.toggleArchiveToggle(); + await runListHarness.showArchived(); expect(await runListHarness.isShowingArchived()).toBeTrue(); await expectRunsIsSelected([false, false, false]); }); @@ -312,7 +312,7 @@ function runArchiveStatusChanged(): void { ]) ); component.ngOnInit(); - await runListHarness.toggleArchiveToggle(); + await runListHarness.showArchived(); expect(await runListHarness.isShowingArchived()).toBeTrue(); expect(await runListHarness.getNumRunListItems()).toEqual(1); await expectRunTitles([run2Title]); diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.ts b/src/app/teacher/teacher-run-list/teacher-run-list.component.ts index 34fba7b51d2..00d4a273133 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.ts +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.ts @@ -199,7 +199,7 @@ export class TeacherRunListComponent implements OnInit { } } - protected isShowArchivedChanged(): void { + protected showArchivedChanged(): void { this.turnOnShowAll(); this.unselectAllRuns(); this.updateNumSelectedRuns(); diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.harness.ts b/src/app/teacher/teacher-run-list/teacher-run-list.harness.ts index c46051272f1..6a99d22f28e 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.harness.ts +++ b/src/app/teacher/teacher-run-list/teacher-run-list.harness.ts @@ -1,25 +1,27 @@ import { ComponentHarness } from '@angular/cdk/testing'; import { MatButtonHarness } from '@angular/material/button/testing'; -import { MatSlideToggleHarness } from '@angular/material/slide-toggle/testing'; import { TeacherRunListItemHarness } from '../teacher-run-list-item/teacher-run-list-item.harness'; import { SelectRunsControlsHarness } from '../select-runs-controls/select-runs-controls.harness'; +import { MatSelectHarness } from '@angular/material/select/testing'; export class TeacherRunListHarness extends ComponentHarness { static hostSelector = 'app-teacher-run-list'; - protected getArchiveButton = this.locatorFor(MatButtonHarness.with({ text: 'Archive Selected' })); - protected getArchiveToggle = this.locatorFor(MatSlideToggleHarness); + protected getArchiveButton = this.locatorFor( + MatButtonHarness.with({ selector: '[aria-label="Archive selected units"]' }) + ); + protected getViewSelect = this.locatorFor(MatSelectHarness); protected getRunListItems = this.locatorForAll(TeacherRunListItemHarness); protected getSelectRunsControls = this.locatorFor(SelectRunsControlsHarness); protected getUnarchiveButton = this.locatorFor( - MatButtonHarness.with({ text: 'Unarchive Selected' }) + MatButtonHarness.with({ selector: '[aria-label="Restore selected units"]' }) ); async isShowingArchived(): Promise { - return (await this.getArchiveToggle()).isChecked(); + return (await (await this.getViewSelect()).getValueText()) === 'true'; } - async toggleArchiveToggle(): Promise { - return (await this.getArchiveToggle()).toggle(); + async showArchived(): Promise { + return await (await this.getViewSelect()).clickOptions({ text: 'Archived' }); } async checkSelectRunsCheckbox(): Promise { diff --git a/src/assets/wise5/authoringTool/peer-grouping/create-new-peer-grouping-dialog/create-new-peer-grouping-dialog.component.ts b/src/assets/wise5/authoringTool/peer-grouping/create-new-peer-grouping-dialog/create-new-peer-grouping-dialog.component.ts index e84a2b14506..5941ce34f45 100644 --- a/src/assets/wise5/authoringTool/peer-grouping/create-new-peer-grouping-dialog/create-new-peer-grouping-dialog.component.ts +++ b/src/assets/wise5/authoringTool/peer-grouping/create-new-peer-grouping-dialog/create-new-peer-grouping-dialog.component.ts @@ -34,7 +34,7 @@ export class CreateNewPeerGroupingDialogComponent extends AuthorPeerGroupingDial this.dialogRef.close(); }, () => { - this.snackBar.open($localize`Please Try Again (Error: Duplicate Tag)`); + this.snackBar.open($localize`Please try again (Error: duplicate tag).`); } ); } diff --git a/src/messages.xlf b/src/messages.xlf index a716f0c1c46..60cb1db96b1 100644 --- a/src/messages.xlf +++ b/src/messages.xlf @@ -8831,6 +8831,20 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.3 + + Active + + src/app/teacher/teacher-run-list/teacher-run-list.component.html + 27 + + + + Archived + + src/app/teacher/teacher-run-list/teacher-run-list.component.html + 28 + + Archived classroom units: From 9edbd45f32e045ed4340b0048241cf63a17845d1 Mon Sep 17 00:00:00 2001 From: Geoffrey Kwan Date: Fri, 25 Aug 2023 12:59:54 -0400 Subject: [PATCH 27/43] fix(Archive): Do not show all and select all includes shared runs #1012 --- .../teacher-run-list-item.harness.ts | 5 +-- .../teacher-run-list.component.ts | 41 ++++++++----------- .../teacher-run-list.harness.ts | 5 ++- 3 files changed, 20 insertions(+), 31 deletions(-) diff --git a/src/app/teacher/teacher-run-list-item/teacher-run-list-item.harness.ts b/src/app/teacher/teacher-run-list-item/teacher-run-list-item.harness.ts index 769122aa9f0..7fad3371c8a 100644 --- a/src/app/teacher/teacher-run-list-item/teacher-run-list-item.harness.ts +++ b/src/app/teacher/teacher-run-list-item/teacher-run-list-item.harness.ts @@ -30,9 +30,6 @@ export class TeacherRunListItemHarness extends ComponentHarness { } async isArchived(): Promise { - const cardWithArchivedRunClass = await this.locatorForOptional( - MatCardHarness.with({ selector: '.archived-run' }) - )(); - return cardWithArchivedRunClass != null; + return (await this.locatorForOptional('.mat-mdc-card-title.warn')()) != null; } } diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.ts b/src/app/teacher/teacher-run-list/teacher-run-list.component.ts index 00d4a273133..bc5e74682e0 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.ts +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.ts @@ -138,7 +138,6 @@ export class TeacherRunListComponent implements OnInit { protected searchChanged(searchValue: string): void { this.searchValue = searchValue; this.performSearchAndFilter(); - this.turnOnShowAll(); } private performFilter(): void { @@ -200,7 +199,6 @@ export class TeacherRunListComponent implements OnInit { } protected showArchivedChanged(): void { - this.turnOnShowAll(); this.unselectAllRuns(); this.updateNumSelectedRuns(); this.performSearchAndFilter(); @@ -213,25 +211,22 @@ export class TeacherRunListComponent implements OnInit { } protected selectRunsOptionChosen(value: string): void { - this.turnOnShowAll(); - this.filteredRuns - .filter((run: TeacherRun) => !run.shared) - .forEach((run: TeacherRun) => { - switch (value) { - case 'all': - run.selected = true; - break; - case 'none': - run.selected = false; - break; - case 'running': - run.selected = !run.isCompleted(this.configService.getCurrentServerTime()); - break; - case 'completed': - run.selected = run.isCompleted(this.configService.getCurrentServerTime()); - break; - } - }); + this.filteredRuns.forEach((run: TeacherRun) => { + switch (value) { + case 'all': + run.selected = true; + break; + case 'none': + run.selected = false; + break; + case 'running': + run.selected = !run.isCompleted(this.configService.getCurrentServerTime()); + break; + case 'completed': + run.selected = run.isCompleted(this.configService.getCurrentServerTime()); + break; + } + }); this.updateNumSelectedRuns(); } @@ -308,8 +303,4 @@ export class TeacherRunListComponent implements OnInit { return run.selected; }); } - - private turnOnShowAll(): void { - this.showAll = true; - } } diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.harness.ts b/src/app/teacher/teacher-run-list/teacher-run-list.harness.ts index 6a99d22f28e..adea7d9226a 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.harness.ts +++ b/src/app/teacher/teacher-run-list/teacher-run-list.harness.ts @@ -6,6 +6,7 @@ import { MatSelectHarness } from '@angular/material/select/testing'; export class TeacherRunListHarness extends ComponentHarness { static hostSelector = 'app-teacher-run-list'; + private ARCHIVED_TEXT = 'Archived'; protected getArchiveButton = this.locatorFor( MatButtonHarness.with({ selector: '[aria-label="Archive selected units"]' }) ); @@ -17,11 +18,11 @@ export class TeacherRunListHarness extends ComponentHarness { ); async isShowingArchived(): Promise { - return (await (await this.getViewSelect()).getValueText()) === 'true'; + return (await (await this.getViewSelect()).getValueText()) === this.ARCHIVED_TEXT; } async showArchived(): Promise { - return await (await this.getViewSelect()).clickOptions({ text: 'Archived' }); + return (await this.getViewSelect()).clickOptions({ text: this.ARCHIVED_TEXT }); } async checkSelectRunsCheckbox(): Promise { From c1196d626a963d13a5b36f8939e2248eb15791d0 Mon Sep 17 00:00:00 2001 From: Geoffrey Kwan Date: Sun, 27 Aug 2023 12:15:01 -0400 Subject: [PATCH 28/43] chore(Archive Run): Move archive button into select run controls #1012 --- .../select-runs-controls.component.html | 27 +++++- .../select-runs-controls.component.spec.ts | 6 +- .../select-runs-controls.component.ts | 85 +++++++++++++++++- .../select-runs-controls.harness.ts | 15 ++++ .../select-runs-controls.module.ts | 2 + .../teacher-run-list.component.html | 48 ++-------- .../teacher-run-list.component.ts | 87 ++----------------- .../teacher-run-list.harness.ts | 13 +-- 8 files changed, 149 insertions(+), 134 deletions(-) diff --git a/src/app/teacher/select-runs-controls/select-runs-controls.component.html b/src/app/teacher/select-runs-controls/select-runs-controls.component.html index a49e1dc68c0..976647eaa78 100644 --- a/src/app/teacher/select-runs-controls/select-runs-controls.component.html +++ b/src/app/teacher/select-runs-controls/select-runs-controls.component.html @@ -1,4 +1,4 @@ -
+
Completed
+
{{ numSelectedRuns }} selected
+ +
diff --git a/src/app/teacher/select-runs-controls/select-runs-controls.component.spec.ts b/src/app/teacher/select-runs-controls/select-runs-controls.component.spec.ts index da5b1689a5b..3c6e1f91411 100644 --- a/src/app/teacher/select-runs-controls/select-runs-controls.component.spec.ts +++ b/src/app/teacher/select-runs-controls/select-runs-controls.component.spec.ts @@ -1,6 +1,9 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { SelectRunsControlsComponent } from './select-runs-controls.component'; import { SelectRunsControlsModule } from './select-runs-controls.module'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; +import { ArchiveProjectService } from '../../services/archive-project.service'; +import { MockArchiveProjectService } from '../../services/mock-archive-project.service'; describe('SelectRunsControlsComponent', () => { let component: SelectRunsControlsComponent; @@ -8,7 +11,8 @@ describe('SelectRunsControlsComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [SelectRunsControlsModule] + imports: [MatSnackBarModule, SelectRunsControlsModule], + providers: [{ provide: ArchiveProjectService, useClass: MockArchiveProjectService }] }).compileComponents(); fixture = TestBed.createComponent(SelectRunsControlsComponent); diff --git a/src/app/teacher/select-runs-controls/select-runs-controls.component.ts b/src/app/teacher/select-runs-controls/select-runs-controls.component.ts index f640aa7f870..abd4c1995ba 100644 --- a/src/app/teacher/select-runs-controls/select-runs-controls.component.ts +++ b/src/app/teacher/select-runs-controls/select-runs-controls.component.ts @@ -1,5 +1,11 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; import { MAT_CHECKBOX_DEFAULT_OPTIONS } from '@angular/material/checkbox'; +import { TeacherRun } from '../teacher-run'; +import { ArchiveProjectResponse } from '../../domain/archiveProjectResponse'; +import { Subscription } from 'rxjs'; +import { Project } from '../../domain/project'; +import { ArchiveProjectService } from '../../services/archive-project.service'; +import { MatSnackBar } from '@angular/material/snack-bar'; @Component({ selector: 'select-runs-controls', @@ -8,17 +14,33 @@ import { MAT_CHECKBOX_DEFAULT_OPTIONS } from '@angular/material/checkbox'; providers: [{ provide: MAT_CHECKBOX_DEFAULT_OPTIONS, useValue: { clickAction: 'noop' } }] }) export class SelectRunsControlsComponent { - @Input() numSelectedRuns: number = 0; - @Input() numTotalRuns: number = 0; + @Output() archiveActionEvent = new EventEmitter(); + protected numSelectedRuns: number = 0; + @Input() runChangedEventEmitter: EventEmitter = new EventEmitter(); + @Input() runs: TeacherRun[] = []; protected selectedAllRuns: boolean = false; protected selectedSomeRuns: boolean = false; @Output() selectRunsOptionChosenEvent = new EventEmitter(); + @Input() showArchived: boolean = false; + + constructor( + private archiveProjectService: ArchiveProjectService, + private snackBar: MatSnackBar + ) {} + + ngOnInit(): void { + this.runChangedEventEmitter.subscribe(() => { + this.ngOnChanges(); + }); + } ngOnChanges(): void { + const numRuns = this.runs.length; + this.numSelectedRuns = this.runs.filter((run: TeacherRun) => run.selected).length; if (this.numSelectedRuns === 0) { this.selectedAllRuns = false; this.selectedSomeRuns = false; - } else if (this.numSelectedRuns === this.numTotalRuns) { + } else if (this.numSelectedRuns === numRuns) { this.selectedAllRuns = true; this.selectedSomeRuns = false; } else { @@ -38,4 +60,61 @@ export class SelectRunsControlsComponent { protected selectRunsOptionChosen(value: string): void { this.selectRunsOptionChosenEvent.emit(value); } + + protected archiveSelectedRuns(): Subscription { + const runs = this.getSelectedRuns(); + return this.archiveProjectService.archiveProjects(this.getProjects(runs)).subscribe({ + next: (archiveProjectsResponse: ArchiveProjectResponse[]) => { + this.updateRunsArchivedStatus(runs, archiveProjectsResponse); + this.snackBar.open( + $localize`Successfully archived ${ + archiveProjectsResponse.filter((response: ArchiveProjectResponse) => response.archived) + .length + } unit(s).` + ); + }, + error: () => { + this.snackBar.open($localize`Error archiving unit(s).`); + } + }); + } + + protected restoreSelectedRuns(): Subscription { + const runs = this.getSelectedRuns(); + return this.archiveProjectService.unarchiveProjects(this.getProjects(runs)).subscribe({ + next: (archiveProjectsResponse: ArchiveProjectResponse[]) => { + this.updateRunsArchivedStatus(runs, archiveProjectsResponse); + this.snackBar.open( + $localize`Successfully restored ${ + archiveProjectsResponse.filter((response: ArchiveProjectResponse) => !response.archived) + .length + } unit(s).` + ); + }, + error: () => { + this.snackBar.open($localize`Error restoring unit(s).`); + } + }); + } + + private updateRunsArchivedStatus( + runs: TeacherRun[], + archiveProjectsResponse: ArchiveProjectResponse[] + ): void { + for (const archiveProjectResponse of archiveProjectsResponse) { + const run = runs.find((run: TeacherRun) => run.project.id === archiveProjectResponse.id); + run.archived = archiveProjectResponse.archived; + } + this.archiveActionEvent.emit(); + } + + private getSelectedRuns(): TeacherRun[] { + return this.runs.filter((run: TeacherRun) => { + return run.selected; + }); + } + + private getProjects(runs: TeacherRun[]): Project[] { + return runs.map((run: TeacherRun) => run.project); + } } diff --git a/src/app/teacher/select-runs-controls/select-runs-controls.harness.ts b/src/app/teacher/select-runs-controls/select-runs-controls.harness.ts index be91ebba806..345f3cf73ef 100644 --- a/src/app/teacher/select-runs-controls/select-runs-controls.harness.ts +++ b/src/app/teacher/select-runs-controls/select-runs-controls.harness.ts @@ -1,11 +1,18 @@ import { ComponentHarness } from '@angular/cdk/testing'; +import { MatButtonHarness } from '@angular/material/button/testing'; import { MatCheckboxHarness } from '@angular/material/checkbox/testing'; import { MatMenuHarness } from '@angular/material/menu/testing'; export class SelectRunsControlsHarness extends ComponentHarness { static hostSelector = 'select-runs-controls'; + protected getArchiveButton = this.locatorFor( + MatButtonHarness.with({ selector: '[aria-label="Archive selected units"]' }) + ); protected getMenu = this.locatorFor(MatMenuHarness); protected getSelectAllCheckbox = this.locatorFor(MatCheckboxHarness); + protected getUnarchiveButton = this.locatorFor( + MatButtonHarness.with({ selector: '[aria-label="Restore selected units"]' }) + ); async checkCheckbox(): Promise { return (await this.getSelectAllCheckbox()).check(); @@ -32,4 +39,12 @@ export class SelectRunsControlsHarness extends ComponentHarness { await menu.open(); return menu.clickItem({ text: menuButtonText }); } + + async clickArchiveButton(): Promise { + return (await this.getArchiveButton()).click(); + } + + async clickUnarchiveButton(): Promise { + return (await this.getUnarchiveButton()).click(); + } } diff --git a/src/app/teacher/select-runs-controls/select-runs-controls.module.ts b/src/app/teacher/select-runs-controls/select-runs-controls.module.ts index 05bbe6425de..d5e28e41dd2 100644 --- a/src/app/teacher/select-runs-controls/select-runs-controls.module.ts +++ b/src/app/teacher/select-runs-controls/select-runs-controls.module.ts @@ -6,11 +6,13 @@ import { SelectRunsControlsComponent } from './select-runs-controls.component'; import { FlexLayoutModule } from '@angular/flex-layout'; import { MatButtonModule } from '@angular/material/button'; import { MatTooltipModule } from '@angular/material/tooltip'; +import { CommonModule } from '@angular/common'; @NgModule({ declarations: [SelectRunsControlsComponent], exports: [SelectRunsControlsComponent], imports: [ + CommonModule, FlexLayoutModule, MatButtonModule, MatCheckboxModule, diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.html b/src/app/teacher/teacher-run-list/teacher-run-list.component.html index 1705a382d19..cf61394a413 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.html +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.html @@ -23,7 +23,7 @@ View - + Active Archived @@ -63,45 +63,13 @@

-
- -
- {{ numSelectedRuns }} selected -
- - -
+ diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.ts b/src/app/teacher/teacher-run-list/teacher-run-list.component.ts index bc5e74682e0..edd23fa9ce7 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.ts +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.ts @@ -1,4 +1,4 @@ -import { Component, Inject, LOCALE_ID, OnInit } from '@angular/core'; +import { Component, EventEmitter, Inject, LOCALE_ID, OnInit } from '@angular/core'; import { TeacherService } from '../teacher.service'; import { TeacherRun } from '../teacher-run'; import { ConfigService } from '../../services/config.service'; @@ -7,10 +7,6 @@ import { formatDate } from '@angular/common'; import { Observable, of, Subscription } from 'rxjs'; import { UserService } from '../../services/user.service'; import { mergeMap } from 'rxjs/operators'; -import { MatSnackBar } from '@angular/material/snack-bar'; -import { ArchiveProjectService } from '../../services/archive-project.service'; -import { Project } from '../../domain/project'; -import { ArchiveProjectResponse } from '../../domain/archiveProjectResponse'; @Component({ selector: 'app-teacher-run-list', @@ -24,6 +20,7 @@ export class TeacherRunListComponent implements OnInit { protected filterValue: string = ''; protected loaded: boolean = false; protected numSelectedRuns: number = 0; + protected runChangedEventEmitter: EventEmitter = new EventEmitter(); protected runs: TeacherRun[] = []; protected searchValue: string = ''; protected showAll: boolean = false; @@ -31,12 +28,10 @@ export class TeacherRunListComponent implements OnInit { private subscriptions: Subscription = new Subscription(); constructor( - private archiveProjectService: ArchiveProjectService, private configService: ConfigService, @Inject(LOCALE_ID) private localeID: string, private route: ActivatedRoute, private router: Router, - private snackBar: MatSnackBar, private teacherService: TeacherService, private userService: UserService ) {} @@ -132,7 +127,7 @@ export class TeacherRunListComponent implements OnInit { private performSearchAndFilter(): void { this.filteredRuns = this.searchValue ? this.performSearch(this.searchValue) : this.runs; this.performFilter(); - this.updateNumSelectedRuns(); + this.runSelectedStatusChanged(); } protected searchChanged(searchValue: string): void { @@ -198,12 +193,6 @@ export class TeacherRunListComponent implements OnInit { } } - protected showArchivedChanged(): void { - this.unselectAllRuns(); - this.updateNumSelectedRuns(); - this.performSearchAndFilter(); - } - private unselectAllRuns(): void { for (const run of this.runs) { run.selected = false; @@ -227,80 +216,20 @@ export class TeacherRunListComponent implements OnInit { break; } }); - this.updateNumSelectedRuns(); - } - - private updateNumSelectedRuns(): void { - this.numSelectedRuns = this.getSelectedRuns().length; - } - - protected archiveSelectedRuns(): Subscription { - const runs = this.getSelectedRuns(); - return this.archiveProjectService.archiveProjects(this.getProjects(runs)).subscribe({ - next: (archiveProjectsResponse: ArchiveProjectResponse[]) => { - this.updateRunsArchivedStatus(runs, archiveProjectsResponse); - this.updateRunsInformation(); - this.snackBar.open( - $localize`Successfully archived ${ - archiveProjectsResponse.filter((response) => response.archived).length - } unit(s).` - ); - }, - error: () => { - this.snackBar.open($localize`Error archiving unit(s).`); - } - }); - } - - protected unarchiveSelectedRuns(): Subscription { - const runs = this.getSelectedRuns(); - return this.archiveProjectService.unarchiveProjects(this.getProjects(runs)).subscribe({ - next: (archiveProjectsResponse: ArchiveProjectResponse[]) => { - this.updateRunsArchivedStatus(runs, archiveProjectsResponse); - this.updateRunsInformation(); - this.snackBar.open( - $localize`Successfully restored ${ - archiveProjectsResponse.filter((response) => !response.archived).length - } unit(s).` - ); - }, - error: () => { - this.snackBar.open($localize`Error restoring unit(s).`); - } - }); - } - - private updateRunsArchivedStatus( - runs: TeacherRun[], - archiveProjectsResponse: ArchiveProjectResponse[] - ): void { - for (const archiveProjectResponse of archiveProjectsResponse) { - const run = runs.find((run: TeacherRun) => run.project.id === archiveProjectResponse.id); - run.archived = archiveProjectResponse.archived; - } + this.runSelectedStatusChanged(); } - private updateRunsInformation(): void { + protected updateRunsInformation(): void { this.unselectAllRuns(); - this.updateNumSelectedRuns(); + this.runSelectedStatusChanged(); this.performSearchAndFilter(); } - private getProjects(runs: TeacherRun[]): Project[] { - return runs.map((run: TeacherRun) => run.project); - } - - protected runSelectedStatusChanged(): void { - this.updateNumSelectedRuns(); - } - protected runArchiveStatusChanged(): void { this.performSearchAndFilter(); } - private getSelectedRuns(): TeacherRun[] { - return this.filteredRuns.filter((run: TeacherRun) => { - return run.selected; - }); + private runSelectedStatusChanged(): void { + this.runChangedEventEmitter.emit(); } } diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.harness.ts b/src/app/teacher/teacher-run-list/teacher-run-list.harness.ts index adea7d9226a..a5c299f143f 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.harness.ts +++ b/src/app/teacher/teacher-run-list/teacher-run-list.harness.ts @@ -1,5 +1,4 @@ import { ComponentHarness } from '@angular/cdk/testing'; -import { MatButtonHarness } from '@angular/material/button/testing'; import { TeacherRunListItemHarness } from '../teacher-run-list-item/teacher-run-list-item.harness'; import { SelectRunsControlsHarness } from '../select-runs-controls/select-runs-controls.harness'; import { MatSelectHarness } from '@angular/material/select/testing'; @@ -7,15 +6,9 @@ import { MatSelectHarness } from '@angular/material/select/testing'; export class TeacherRunListHarness extends ComponentHarness { static hostSelector = 'app-teacher-run-list'; private ARCHIVED_TEXT = 'Archived'; - protected getArchiveButton = this.locatorFor( - MatButtonHarness.with({ selector: '[aria-label="Archive selected units"]' }) - ); - protected getViewSelect = this.locatorFor(MatSelectHarness); protected getRunListItems = this.locatorForAll(TeacherRunListItemHarness); protected getSelectRunsControls = this.locatorFor(SelectRunsControlsHarness); - protected getUnarchiveButton = this.locatorFor( - MatButtonHarness.with({ selector: '[aria-label="Restore selected units"]' }) - ); + protected getViewSelect = this.locatorFor(MatSelectHarness); async isShowingArchived(): Promise { return (await (await this.getViewSelect()).getValueText()) === this.ARCHIVED_TEXT; @@ -84,10 +77,10 @@ export class TeacherRunListHarness extends ComponentHarness { } async clickArchiveButton(): Promise { - return (await this.getArchiveButton()).click(); + return (await this.getSelectRunsControls()).clickArchiveButton(); } async clickUnarchiveButton(): Promise { - return (await this.getUnarchiveButton()).click(); + return (await this.getSelectRunsControls()).clickUnarchiveButton(); } } From 52d13503afdbfdf13a661a49aa0a57e48e97d22a Mon Sep 17 00:00:00 2001 From: Geoffrey Kwan Date: Mon, 28 Aug 2023 12:44:50 -0400 Subject: [PATCH 29/43] feat(Archive Run): Allow undoing an archive action #1012 --- src/app/services/archive-project.service.ts | 9 ++- .../services/mock-archive-project.service.ts | 9 ++- .../run-menu/run-menu.component.spec.ts | 40 +++++++++++-- .../teacher/run-menu/run-menu.component.ts | 60 ++++++++++++------- .../select-runs-controls.component.ts | 47 +++++++++++---- .../teacher-run-list.component.ts | 11 ++++ 6 files changed, 136 insertions(+), 40 deletions(-) diff --git a/src/app/services/archive-project.service.ts b/src/app/services/archive-project.service.ts index aaf62ec6735..fc559f1b5e3 100644 --- a/src/app/services/archive-project.service.ts +++ b/src/app/services/archive-project.service.ts @@ -1,11 +1,14 @@ import { HttpClient, HttpParams } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { Observable } from 'rxjs'; +import { Observable, Subject } from 'rxjs'; import { Project } from '../domain/project'; import { ArchiveProjectResponse } from '../domain/archiveProjectResponse'; @Injectable() export class ArchiveProjectService { + private refreshProjectsEventSource: Subject = new Subject(); + public refreshProjectsEvent$ = this.refreshProjectsEventSource.asObservable(); + constructor(private http: HttpClient) {} archiveProject(project: Project): Observable { @@ -30,4 +33,8 @@ export class ArchiveProjectService { params: params }); } + + refreshProjects(): void { + this.refreshProjectsEventSource.next(); + } } diff --git a/src/app/services/mock-archive-project.service.ts b/src/app/services/mock-archive-project.service.ts index 584b38a0ec9..0432381ed35 100644 --- a/src/app/services/mock-archive-project.service.ts +++ b/src/app/services/mock-archive-project.service.ts @@ -1,8 +1,11 @@ -import { Observable, of } from 'rxjs'; +import { Observable, Subject, of } from 'rxjs'; import { Project } from '../domain/project'; import { ArchiveProjectResponse } from '../domain/archiveProjectResponse'; export class MockArchiveProjectService { + private refreshProjectsEventSource: Subject = new Subject(); + public refreshProjectsEvent$ = this.refreshProjectsEventSource.asObservable(); + archiveProject(project: Project): Observable { project.archived = true; return of(project); @@ -22,4 +25,8 @@ export class MockArchiveProjectService { projects.forEach((project) => (project.archived = false)); return of(projects); } + + refreshProjects(): void { + this.refreshProjectsEventSource.next(); + } } diff --git a/src/app/teacher/run-menu/run-menu.component.spec.ts b/src/app/teacher/run-menu/run-menu.component.spec.ts index bdef07f28fd..90e4a728c26 100644 --- a/src/app/teacher/run-menu/run-menu.component.spec.ts +++ b/src/app/teacher/run-menu/run-menu.component.spec.ts @@ -20,6 +20,8 @@ import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MockArchiveProjectService } from '../../services/mock-archive-project.service'; +import { MatSnackBarHarness } from '@angular/material/snack-bar/testing'; +import { HarnessLoader } from '@angular/cdk/testing'; export class MockTeacherService { checkClassroomAuthorization(): Observable { @@ -68,8 +70,8 @@ let archiveProjectService: ArchiveProjectService; let component: RunMenuComponent; let fixture: ComponentFixture; const owner = new User(); +let rootLoader: HarnessLoader; let runMenuHarness: RunMenuHarness; -let snackBarSpy: jasmine.Spy; let teacherService: TeacherService; describe('RunMenuComponent', () => { @@ -104,9 +106,9 @@ describe('RunMenuComponent', () => { setRun(false); archiveProjectService = TestBed.inject(ArchiveProjectService); teacherService = TestBed.inject(TeacherService); - snackBarSpy = spyOn(TestBed.inject(MatSnackBar), 'open'); fixture.detectChanges(); runMenuHarness = await TestbedHarnessEnvironment.harnessForFixture(fixture, RunMenuHarness); + rootLoader = TestbedHarnessEnvironment.documentRootLoader(fixture); }); archive(); @@ -132,7 +134,19 @@ function archive() { it('should archive a run', async () => { await runMenuHarness.clickArchiveMenuButton(); expect(component.run.archived).toEqual(true); - expect(snackBarSpy).toHaveBeenCalledWith('Successfully archived unit.'); + const snackBar = await getSnackBar(); + expect(await snackBar.getMessage()).toEqual('Successfully archived unit.'); + }); + it('should archive a run and then undo', async () => { + await runMenuHarness.clickArchiveMenuButton(); + expect(component.run.archived).toEqual(true); + let snackBar = await getSnackBar(); + expect(await snackBar.getMessage()).toEqual('Successfully archived unit.'); + expect(await snackBar.getActionDescription()).toEqual('Undo'); + await snackBar.dismissWithAction(); + expect(component.run.archived).toEqual(false); + snackBar = await getSnackBar(); + expect(await snackBar.getMessage()).toEqual('Action undone.'); }); }); } @@ -144,7 +158,25 @@ function unarchive() { component.ngOnInit(); await runMenuHarness.clickUnarchiveMenuButton(); expect(component.run.archived).toEqual(false); - expect(snackBarSpy).toHaveBeenCalledWith('Successfully restored unit.'); + const snackBar = await getSnackBar(); + expect(await snackBar.getMessage()).toEqual('Successfully restored unit.'); + }); + it('should unarchive a run and then undo', async () => { + setRun(true); + component.ngOnInit(); + await runMenuHarness.clickUnarchiveMenuButton(); + expect(component.run.archived).toEqual(false); + let snackBar = await getSnackBar(); + expect(await snackBar.getMessage()).toEqual('Successfully restored unit.'); + expect(await snackBar.getActionDescription()).toEqual('Undo'); + await snackBar.dismissWithAction(); + expect(component.run.archived).toEqual(true); + snackBar = await getSnackBar(); + expect(await snackBar.getMessage()).toEqual('Action undone.'); }); }); } + +async function getSnackBar() { + return await rootLoader.getHarness(MatSnackBarHarness); +} diff --git a/src/app/teacher/run-menu/run-menu.component.ts b/src/app/teacher/run-menu/run-menu.component.ts index 236ecaeb061..2c9b410bd59 100644 --- a/src/app/teacher/run-menu/run-menu.component.ts +++ b/src/app/teacher/run-menu/run-menu.component.ts @@ -84,39 +84,55 @@ export class RunMenuComponent implements OnInit { } } - protected archive(): void { - this.performArchiveAction( - this.run, - 'archiveProject', - $localize`Successfully archived unit.`, - $localize`Error archiving unit.` - ); + const run = this.run; + this.archiveProjectService.archiveProject(run.project).subscribe({ + next: (response: ArchiveProjectResponse) => { + this.updateArchivedStatus(run, response.archived); + this.openSnackBar(run, $localize`Successfully archived unit.`, 'unarchiveProject'); + }, + error: () => { + this.snackBar.open($localize`Error archiving unit.`); + } + }); } protected unarchive(): void { - this.performArchiveAction( - this.run, - 'unarchiveProject', - $localize`Successfully restored unit.`, - $localize`Error restoring unit.` - ); + const run = this.run; + this.archiveProjectService.unarchiveProject(run.project).subscribe({ + next: (response: ArchiveProjectResponse) => { + this.updateArchivedStatus(run, response.archived); + this.openSnackBar(run, $localize`Successfully restored unit.`, 'archiveProject'); + }, + error: () => { + this.snackBar.open($localize`Error restoring unit.`); + } + }); + } + + private updateArchivedStatus(run: TeacherRun, archived: boolean): void { + run.archived = archived; + this.runArchiveStatusChangedEvent.emit(run); + } + + private openSnackBar(run: TeacherRun, message: string, undoFunctionName: string): void { + this.snackBar + .open(message, $localize`Undo`) + .onAction() + .subscribe(() => { + this.undoArchiveAction(run, undoFunctionName); + }); } - private performArchiveAction( - run: TeacherRun, - archiveFunctionName: 'archiveProject' | 'unarchiveProject', - successMessage: string, - errorMessage: string - ): void { + private undoArchiveAction(run: TeacherRun, archiveFunctionName: string): void { this.archiveProjectService[archiveFunctionName](run.project).subscribe({ next: (response: ArchiveProjectResponse) => { run.archived = response.archived; - this.runArchiveStatusChangedEvent.emit(run); - this.snackBar.open(successMessage); + this.archiveProjectService.refreshProjects(); + this.snackBar.open($localize`Action undone.`); }, error: () => { - this.snackBar.open(errorMessage); + this.snackBar.open($localize`Error undoing action.`); } }); } diff --git a/src/app/teacher/select-runs-controls/select-runs-controls.component.ts b/src/app/teacher/select-runs-controls/select-runs-controls.component.ts index abd4c1995ba..5e4c85e2550 100644 --- a/src/app/teacher/select-runs-controls/select-runs-controls.component.ts +++ b/src/app/teacher/select-runs-controls/select-runs-controls.component.ts @@ -66,12 +66,7 @@ export class SelectRunsControlsComponent { return this.archiveProjectService.archiveProjects(this.getProjects(runs)).subscribe({ next: (archiveProjectsResponse: ArchiveProjectResponse[]) => { this.updateRunsArchivedStatus(runs, archiveProjectsResponse); - this.snackBar.open( - $localize`Successfully archived ${ - archiveProjectsResponse.filter((response: ArchiveProjectResponse) => response.archived) - .length - } unit(s).` - ); + this.openSuccessSnackBar(runs, archiveProjectsResponse, true); }, error: () => { this.snackBar.open($localize`Error archiving unit(s).`); @@ -84,12 +79,7 @@ export class SelectRunsControlsComponent { return this.archiveProjectService.unarchiveProjects(this.getProjects(runs)).subscribe({ next: (archiveProjectsResponse: ArchiveProjectResponse[]) => { this.updateRunsArchivedStatus(runs, archiveProjectsResponse); - this.snackBar.open( - $localize`Successfully restored ${ - archiveProjectsResponse.filter((response: ArchiveProjectResponse) => !response.archived) - .length - } unit(s).` - ); + this.openSuccessSnackBar(runs, archiveProjectsResponse, false); }, error: () => { this.snackBar.open($localize`Error restoring unit(s).`); @@ -108,6 +98,39 @@ export class SelectRunsControlsComponent { this.archiveActionEvent.emit(); } + private openSuccessSnackBar( + runs: TeacherRun[], + archiveProjectsResponse: ArchiveProjectResponse[], + archived: boolean + ): void { + this.snackBar + .open( + $localize`Successfully ${archived ? 'archived' : 'restored'} ${ + archiveProjectsResponse.filter( + (response: ArchiveProjectResponse) => response.archived === archived + ).length + } unit(s).`, + $localize`Undo` + ) + .onAction() + .subscribe(() => { + this.undoArchiveAction(runs, archived ? 'unarchiveProjects' : 'archiveProjects'); + }); + } + + private undoArchiveAction(runs: TeacherRun[], archiveFunctionName: string): void { + this.archiveProjectService[archiveFunctionName](this.getProjects(runs)).subscribe({ + next: (archiveProjectsResponse: ArchiveProjectResponse[]) => { + this.updateRunsArchivedStatus(runs, archiveProjectsResponse); + this.archiveProjectService.refreshProjects(); + this.snackBar.open($localize`Action undone.`); + }, + error: () => { + this.snackBar.open($localize`Error undoing action.`); + } + }); + } + private getSelectedRuns(): TeacherRun[] { return this.runs.filter((run: TeacherRun) => { return run.selected; diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.ts b/src/app/teacher/teacher-run-list/teacher-run-list.component.ts index edd23fa9ce7..8249d8c3d66 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.ts +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.ts @@ -7,6 +7,7 @@ import { formatDate } from '@angular/common'; import { Observable, of, Subscription } from 'rxjs'; import { UserService } from '../../services/user.service'; import { mergeMap } from 'rxjs/operators'; +import { ArchiveProjectService } from '../../services/archive-project.service'; @Component({ selector: 'app-teacher-run-list', @@ -28,6 +29,7 @@ export class TeacherRunListComponent implements OnInit { private subscriptions: Subscription = new Subscription(); constructor( + private archiveProjectService: ArchiveProjectService, private configService: ConfigService, @Inject(LOCALE_ID) private localeID: string, private route: ActivatedRoute, @@ -39,6 +41,7 @@ export class TeacherRunListComponent implements OnInit { ngOnInit() { this.getRuns(); this.subscribeToRuns(); + this.subscribeToRefreshProjects(); } ngOnDestroy() { @@ -85,6 +88,14 @@ export class TeacherRunListComponent implements OnInit { ); } + private subscribeToRefreshProjects(): void { + this.subscriptions.add( + this.archiveProjectService.refreshProjectsEvent$.subscribe(() => { + this.runArchiveStatusChanged(); + }) + ); + } + private updateExistingRun(updatedRun: TeacherRun): void { const runIndex = this.runs.findIndex((run) => run.id === updatedRun.id); this.runs.splice(runIndex, 1, updatedRun); From 986132c6f5190a169cfeb998c5754e35dde808cc Mon Sep 17 00:00:00 2001 From: Geoffrey Kwan Date: Mon, 28 Aug 2023 14:34:07 -0400 Subject: [PATCH 30/43] chore(Archive Run): Change function name from restore to unarchive #1012 --- .../select-runs-controls/select-runs-controls.component.html | 2 +- .../select-runs-controls/select-runs-controls.component.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/teacher/select-runs-controls/select-runs-controls.component.html b/src/app/teacher/select-runs-controls/select-runs-controls.component.html index 976647eaa78..12b7699ef9b 100644 --- a/src/app/teacher/select-runs-controls/select-runs-controls.component.html +++ b/src/app/teacher/select-runs-controls/select-runs-controls.component.html @@ -40,7 +40,7 @@ -
+ + + + + + {{ run.startTime | date: 'mediumDate' }} + + - {{ run.endTime | date: 'mediumDate' }} + + + + + + + + +
+ +
+
diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts b/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts index a8fa12b5c3f..299f8803df0 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts @@ -134,6 +134,7 @@ describe('TeacherRunListComponent', () => { selectRunsOptionChosen(); sortByStartTimeDesc(); unarchiveSelectedRuns(); + noRuns(); }); function sortByStartTimeDesc() { @@ -345,6 +346,32 @@ function unarchiveRunNoLongerInArchivedView() { }); } +function noRuns(): void { + describe('when there are no runs', () => { + beforeEach(() => { + getRunsSpy.and.returnValue(of([])); + component.ngOnInit(); + }); + describe('in the active view', () => { + it('should display a message', async () => { + expect(await runListHarness.isShowingArchived()).toBeFalse(); + expect(await runListHarness.getNoRunsMessage()).toContain( + "Looks like you don't have any active WISE units in your classes." + ); + }); + }); + describe('in the archived view', () => { + it('should display a message', async () => { + await runListHarness.showArchived(); + expect(await runListHarness.isShowingArchived()).toBeTrue(); + expect(await runListHarness.getNoRunsMessage()).toContain( + "Looks like you don't have any archived WISE units." + ); + }); + }); + }); +} + async function expectRunTitles(expectedRunTitles: string[]): Promise { const numRunListItems = await runListHarness.getNumRunListItems(); for (let i = 0; i < numRunListItems; i++) { diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.harness.ts b/src/app/teacher/teacher-run-list/teacher-run-list.harness.ts index 23a07da192e..10e93607344 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.harness.ts +++ b/src/app/teacher/teacher-run-list/teacher-run-list.harness.ts @@ -7,6 +7,7 @@ import { clickMenuButton } from '../../common/harness-helper'; export class TeacherRunListHarness extends ComponentHarness { static hostSelector = 'app-teacher-run-list'; private ARCHIVED_TEXT = 'Archived'; + protected getNoRunsMessageDiv = this.locatorFor('.no-runs-message'); protected getRunListItems = this.locatorForAll(TeacherRunListItemHarness); protected getSelectRunsControls = this.locatorFor(SelectRunsControlsHarness); protected getViewSelect = this.locatorFor(MatSelectHarness); @@ -84,4 +85,8 @@ export class TeacherRunListHarness extends ComponentHarness { async clickUnarchiveButton(): Promise { return (await this.getSelectRunsControls()).clickUnarchiveButton(); } + + async getNoRunsMessage(): Promise { + return (await this.getNoRunsMessageDiv()).text(); + } } From 728a994bfd2824c113d78e408c12c637fe7700c9 Mon Sep 17 00:00:00 2001 From: Geoffrey Kwan Date: Tue, 29 Aug 2023 16:28:22 -0400 Subject: [PATCH 33/43] feat(Archive Run): Hide run count when there are no runs Slightly change message when there are no runs. #1012 --- .../teacher-run-list/teacher-run-list.component.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.html b/src/app/teacher/teacher-run-list/teacher-run-list.component.html index 2dbd5f875c9..26c32d82308 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.html +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.html @@ -25,7 +25,7 @@
-
+

Units found: {{ filteredRuns.length }}

-

Hey there! Looks like you don't have any active WISE units in your classes.

+

Hey there! Looks like you don't have any active classroom units.

Browse the "Unit Library" to find titles to use with your students.

-

Hey there! Looks like you don't have any archived WISE units.

+

Looks like you don't have any archived classroom units.

Date: Tue, 29 Aug 2023 17:19:27 -0400 Subject: [PATCH 34/43] feat(Archive Run): Add scheduled option to select runs drop down #1012 --- .../select-runs-controls.component.html | 3 +- .../teacher-run-list.component.spec.ts | 140 ++++++++++++------ .../teacher-run-list.component.ts | 9 +- 3 files changed, 100 insertions(+), 52 deletions(-) diff --git a/src/app/teacher/select-runs-controls/select-runs-controls.component.html b/src/app/teacher/select-runs-controls/select-runs-controls.component.html index 42c5747951a..61b6fcaa62a 100644 --- a/src/app/teacher/select-runs-controls/select-runs-controls.component.html +++ b/src/app/teacher/select-runs-controls/select-runs-controls.component.html @@ -20,8 +20,9 @@ - + +
{{ numSelectedRuns }} selected
diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts b/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts index 299f8803df0..0fa4900f826 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts @@ -196,24 +196,21 @@ function showArchivedChanged(): void { function runSelectedStatusChanged(): void { describe('runSelectedStatusChanged()', () => { describe('one run is selected', () => { - it('should show 1 run selected and indeterminate for the select all checkbox', async () => { + it('should show indeterminate for the select all checkbox', async () => { await clickRunListeItemCheckboxes([0]); expect(await runListHarness.isSelectRunsCheckboxIndeterminate()).toBeTrue(); - expect(await runListHarness.getNumSelectedRunListItems()).toEqual(1); }); }); describe('two runs are selected', () => { - it('should show 2 runs selected and indeterminate for the select all checkbox', async () => { + it('should show indeterminate for the select all checkbox', async () => { await clickRunListeItemCheckboxes([0, 1]); expect(await runListHarness.isSelectRunsCheckboxIndeterminate()).toBeTrue(); - expect(await runListHarness.getNumSelectedRunListItems()).toEqual(2); }); }); describe('all runs are selected', () => { - it('should show 3 runs selected and checked for the select all checkbox', async () => { + it('should show checked for the select all checkbox', async () => { await clickRunListeItemCheckboxes([0, 1, 2]); expect(await runListHarness.isSelectRunsCheckboxChecked()).toBeTrue(); - expect(await runListHarness.getNumSelectedRunListItems()).toEqual(3); }); }); }); @@ -235,7 +232,7 @@ function selectAllRunsCheckboxClicked(): void { function selectAllRuns() { describe('select all runs checkbox is not checked and it is clicked', () => { - it('it should select all runs', async () => { + it('should select all runs', async () => { await expectRunsIsSelected([false, false, false]); expect(await runListHarness.isSelectRunsCheckboxChecked()).toBeFalse(); await runListHarness.checkSelectRunsCheckbox(); @@ -247,7 +244,7 @@ function selectAllRuns() { function unselectAllRuns() { describe('select all runs checkbox is checked and it is clicked', () => { - it('it should unselect all runs', async () => { + it('should unselect all runs', async () => { await runListHarness.checkSelectRunsCheckbox(); await expectRunsIsSelected([true, true, true]); expect(await runListHarness.isSelectRunsCheckboxChecked()).toBeTrue(); @@ -260,7 +257,7 @@ function unselectAllRuns() { function someSelectedUnselectAllRuns() { describe('select all runs checkbox is indeterminate checked and it is clicked', () => { - it('it should unselect all runs', async () => { + it('should unselect all runs', async () => { await runListHarness.clickRunListItemCheckbox(0); await expectRunsIsSelected([true, false, false]); expect(await runListHarness.isSelectRunsCheckboxIndeterminate()).toBeTrue(); @@ -273,26 +270,58 @@ function someSelectedUnselectAllRuns() { function selectRunsOptionChosen(): void { describe('selectRunsOptionChosen()', () => { - it('when all is chosen, it should select all runs2', async () => { + selectAllRunsOptionChosen(); + selectNoneOptionChosen(); + selectCompletedOptionChosen(); + selectRunningOptionChosen(); + selectScheduledOptionChosen(); + }); +} + +function selectAllRunsOptionChosen(): void { + describe('when all is chosen', () => { + it('should select all runs', async () => { await runListHarness.clickSelectRunsMenuButton('All'); await expectRunsIsSelected([true, true, true]); }); - it('when none is chosen, it should select no runs', async () => { + }); +} + +function selectNoneOptionChosen(): void { + describe('when none is chosen', () => { + it('should select no runs', async () => { await runListHarness.clickSelectRunsMenuButton('None'); await expectRunsIsSelected([false, false, false]); }); - describe('when a run is completed', () => { - beforeEach(() => { - setRun2Completed(); - }); - it('when running is chosen, it should select running runs', async () => { - await runListHarness.clickSelectRunsMenuButton('Running'); - await expectRunsIsSelected([true, false, true]); - }); - it('when completed is chosen, it should select completed runs', async () => { - await runListHarness.clickSelectRunsMenuButton('Completed'); - await expectRunsIsSelected([false, true, false]); - }); + }); +} + +function selectCompletedOptionChosen(): void { + describe('when completed is chosen', () => { + it('should select completed runs', async () => { + setRun2Completed(); + await runListHarness.clickSelectRunsMenuButton('Completed'); + await expectRunsIsSelected([false, true, false]); + }); + }); +} + +function selectRunningOptionChosen(): void { + describe('when running is chosen', () => { + it('should select running runs', async () => { + setRun2Completed(); + await runListHarness.clickSelectRunsMenuButton('Running'); + await expectRunsIsSelected([true, false, true]); + }); + }); +} + +function selectScheduledOptionChosen(): void { + describe('when scheduled is chosen', () => { + it('it should select scheduled runs', async () => { + setRun3Scheduled(); + await runListHarness.clickSelectRunsMenuButton('Scheduled'); + await expectRunsIsSelected([true, false, false]); }); }); } @@ -308,6 +337,17 @@ function setRun2Completed(): void { component.ngOnInit(); } +function setRun3Scheduled(): void { + getRunsSpy.and.returnValue( + of([ + new TeacherRunStub(1, run1StartTime, null, run1Title), + new TeacherRunStub(2, run2StartTime, null, run2Title), + new TeacherRunStub(3, currentTime + 86400000, null, run3Title) + ]) + ); + component.ngOnInit(); +} + function runArchiveStatusChanged(): void { describe('runArchiveStatusChanged()', () => { archiveRunNoLongerInActiveView(); @@ -316,33 +356,37 @@ function runArchiveStatusChanged(): void { } function archiveRunNoLongerInActiveView() { - it('when a run is archived, it should no longer be displayed in the active view', async () => { - expect(await runListHarness.isShowingArchived()).toBeFalse(); - expect(await runListHarness.getNumRunListItems()).toEqual(3); - await runListHarness.clickRunListItemMenuArchiveButton(1); - expect(await runListHarness.isShowingArchived()).toBeFalse(); - expect(await runListHarness.getNumRunListItems()).toEqual(2); - await expectRunTitles([run3Title, run1Title]); + describe('when a run is archived', () => { + it('it should no longer be displayed in the active view', async () => { + expect(await runListHarness.isShowingArchived()).toBeFalse(); + expect(await runListHarness.getNumRunListItems()).toEqual(3); + await runListHarness.clickRunListItemMenuArchiveButton(1); + expect(await runListHarness.isShowingArchived()).toBeFalse(); + expect(await runListHarness.getNumRunListItems()).toEqual(2); + await expectRunTitles([run3Title, run1Title]); + }); }); } function unarchiveRunNoLongerInArchivedView() { - it('when a run is unarchived, it should no longer be displayed in the archived view', async () => { - getRunsSpy.and.returnValue( - of([ - new TeacherRunStub(1, run1StartTime, null, run1Title), - new TeacherRunStub(2, run2StartTime, null, run2Title, ['archived']), - new TeacherRunStub(3, run3StartTime, null, run3Title) - ]) - ); - component.ngOnInit(); - await runListHarness.showArchived(); - expect(await runListHarness.isShowingArchived()).toBeTrue(); - expect(await runListHarness.getNumRunListItems()).toEqual(1); - await expectRunTitles([run2Title]); - await runListHarness.clickRunListItemMenuUnarchiveButton(0); - expect(await runListHarness.isShowingArchived()).toBeTrue(); - expect(await runListHarness.getNumRunListItems()).toEqual(0); + describe('when a run is unarchived', () => { + it('it should no longer be displayed in the archived view', async () => { + getRunsSpy.and.returnValue( + of([ + new TeacherRunStub(1, run1StartTime, null, run1Title), + new TeacherRunStub(2, run2StartTime, null, run2Title, ['archived']), + new TeacherRunStub(3, run3StartTime, null, run3Title) + ]) + ); + component.ngOnInit(); + await runListHarness.showArchived(); + expect(await runListHarness.isShowingArchived()).toBeTrue(); + expect(await runListHarness.getNumRunListItems()).toEqual(1); + await expectRunTitles([run2Title]); + await runListHarness.clickRunListItemMenuUnarchiveButton(0); + expect(await runListHarness.isShowingArchived()).toBeTrue(); + expect(await runListHarness.getNumRunListItems()).toEqual(0); + }); }); } @@ -356,7 +400,7 @@ function noRuns(): void { it('should display a message', async () => { expect(await runListHarness.isShowingArchived()).toBeFalse(); expect(await runListHarness.getNoRunsMessage()).toContain( - "Looks like you don't have any active WISE units in your classes." + "Hey there! Looks like you don't have any active classroom units." ); }); }); @@ -365,7 +409,7 @@ function noRuns(): void { await runListHarness.showArchived(); expect(await runListHarness.isShowingArchived()).toBeTrue(); expect(await runListHarness.getNoRunsMessage()).toContain( - "Looks like you don't have any archived WISE units." + "Looks like you don't have any archived classroom units." ); }); }); diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.ts b/src/app/teacher/teacher-run-list/teacher-run-list.component.ts index d440c8c9428..cee5f2d5397 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.ts +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.ts @@ -220,12 +220,15 @@ export class TeacherRunListComponent implements OnInit { case 'none': run.selected = false; break; - case 'running': - run.selected = !run.isCompleted(this.configService.getCurrentServerTime()); - break; case 'completed': run.selected = run.isCompleted(this.configService.getCurrentServerTime()); break; + case 'running': + run.selected = run.isActive(this.configService.getCurrentServerTime()); + break; + case 'scheduled': + run.selected = run.isScheduled(this.configService.getCurrentServerTime()); + break; } }); this.runSelectedStatusChanged(); From e3a6f88388bd35eb7c925ba6bc4c5a4ec9ec01fa Mon Sep 17 00:00:00 2001 From: Geoffrey Kwan Date: Tue, 29 Aug 2023 17:25:24 -0400 Subject: [PATCH 35/43] chore(Archive Run): Clean up code #1012 --- src/app/teacher/run-menu/run-menu.component.ts | 4 ++-- .../teacher-run-list-item/teacher-run-list-item.component.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/teacher/run-menu/run-menu.component.ts b/src/app/teacher/run-menu/run-menu.component.ts index b908b249cbe..1ac69fe7886 100644 --- a/src/app/teacher/run-menu/run-menu.component.ts +++ b/src/app/teacher/run-menu/run-menu.component.ts @@ -21,7 +21,7 @@ export class RunMenuComponent implements OnInit { private editLink: string = ''; protected reportProblemLink: string = ''; @Input() run: TeacherRun; - @Output() runArchiveStatusChangedEvent: EventEmitter = new EventEmitter(); + @Output() runArchiveStatusChangedEvent: EventEmitter = new EventEmitter(); constructor( private archiveProjectService: ArchiveProjectService, @@ -112,7 +112,7 @@ export class RunMenuComponent implements OnInit { private updateArchivedStatus(run: TeacherRun, archived: boolean): void { run.archived = archived; - this.runArchiveStatusChangedEvent.emit(run); + this.runArchiveStatusChangedEvent.emit(); } private openSnackBar(run: TeacherRun, message: string, undoFunctionName: string): void { diff --git a/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.ts b/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.ts index 08843756bfb..499651709fa 100644 --- a/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.ts +++ b/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.ts @@ -20,8 +20,8 @@ export class TeacherRunListItemComponent implements OnInit { protected manageStudentsLink: string = ''; protected periodsTooltipText: string; @Input() run: TeacherRun = new TeacherRun(); - @Output() runArchiveStatusChangedEvent: EventEmitter = new EventEmitter(); - @Output() runSelectedStatusChangedEvent: EventEmitter = new EventEmitter(); + @Output() runArchiveStatusChangedEvent: EventEmitter = new EventEmitter(); + @Output() runSelectedStatusChangedEvent: EventEmitter = new EventEmitter(); protected thumbStyle: SafeStyle; constructor( From b11404e46101059e380d77f274f3e60c9af02c56 Mon Sep 17 00:00:00 2001 From: Geoffrey Kwan Date: Tue, 29 Aug 2023 17:39:20 -0400 Subject: [PATCH 36/43] chore(Archive Run): Clean up tests #1012 --- .../teacher-run-list.component.spec.ts | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts b/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts index 0fa4900f826..32460adc2f6 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts @@ -270,7 +270,7 @@ function someSelectedUnselectAllRuns() { function selectRunsOptionChosen(): void { describe('selectRunsOptionChosen()', () => { - selectAllRunsOptionChosen(); + selectAllOptionChosen(); selectNoneOptionChosen(); selectCompletedOptionChosen(); selectRunningOptionChosen(); @@ -278,11 +278,10 @@ function selectRunsOptionChosen(): void { }); } -function selectAllRunsOptionChosen(): void { +function selectAllOptionChosen(): void { describe('when all is chosen', () => { it('should select all runs', async () => { - await runListHarness.clickSelectRunsMenuButton('All'); - await expectRunsIsSelected([true, true, true]); + await clickSelectRunsMenuButtonAndExpectSelected('All', [true, true, true]); }); }); } @@ -290,8 +289,7 @@ function selectAllRunsOptionChosen(): void { function selectNoneOptionChosen(): void { describe('when none is chosen', () => { it('should select no runs', async () => { - await runListHarness.clickSelectRunsMenuButton('None'); - await expectRunsIsSelected([false, false, false]); + await clickSelectRunsMenuButtonAndExpectSelected('None', [false, false, false]); }); }); } @@ -300,8 +298,7 @@ function selectCompletedOptionChosen(): void { describe('when completed is chosen', () => { it('should select completed runs', async () => { setRun2Completed(); - await runListHarness.clickSelectRunsMenuButton('Completed'); - await expectRunsIsSelected([false, true, false]); + await clickSelectRunsMenuButtonAndExpectSelected('Completed', [false, true, false]); }); }); } @@ -310,8 +307,7 @@ function selectRunningOptionChosen(): void { describe('when running is chosen', () => { it('should select running runs', async () => { setRun2Completed(); - await runListHarness.clickSelectRunsMenuButton('Running'); - await expectRunsIsSelected([true, false, true]); + await clickSelectRunsMenuButtonAndExpectSelected('Running', [true, false, true]); }); }); } @@ -320,12 +316,19 @@ function selectScheduledOptionChosen(): void { describe('when scheduled is chosen', () => { it('it should select scheduled runs', async () => { setRun3Scheduled(); - await runListHarness.clickSelectRunsMenuButton('Scheduled'); - await expectRunsIsSelected([true, false, false]); + await clickSelectRunsMenuButtonAndExpectSelected('Scheduled', [true, false, false]); }); }); } +async function clickSelectRunsMenuButtonAndExpectSelected( + menuButtonText: string, + selectedRuns: boolean[] +): Promise { + await runListHarness.clickSelectRunsMenuButton(menuButtonText); + await expectRunsIsSelected(selectedRuns); +} + function setRun2Completed(): void { getRunsSpy.and.returnValue( of([ From 1239af017503e49e71770cfb68f985009279c66d Mon Sep 17 00:00:00 2001 From: Jonathan Lim-Breitbart Date: Wed, 30 Aug 2023 10:30:19 -0700 Subject: [PATCH 37/43] Remove warn color text in archived view --- .../teacher-run-list-item.component.html | 5 +- .../teacher-run-list.component.html | 4 +- src/messages.xlf | 215 ++++++++++-------- 3 files changed, 129 insertions(+), 95 deletions(-) diff --git a/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html b/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html index e45b09fe018..067aeff0b73 100644 --- a/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html +++ b/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html @@ -75,10 +75,7 @@ >
- + {{ run.project.name }}
-
+

Units found: {{ filteredRuns.length }}Hey there! Looks like you don't have any active classroom units.

Browse the "Unit Library" to find titles to use with your students.

-
+

Looks like you don't have any archived classroom units.

diff --git a/src/messages.xlf b/src/messages.xlf index 60cb1db96b1..c5050e6be1e 100644 --- a/src/messages.xlf +++ b/src/messages.xlf @@ -2968,7 +2968,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/app/teacher/teacher-run-list/teacher-run-list.component.html - 18 + 14 src/assets/wise5/authoringTool/addNode/choose-simulation/choose-simulation.component.html @@ -5480,7 +5480,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html - 88 + 85 @@ -6182,7 +6182,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/app/teacher/teacher-run-list/teacher-run-list.component.html - 125 + 104 @@ -6304,6 +6304,10 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.src/app/teacher/run-menu/run-menu.component.html 38 + + src/app/teacher/select-runs-controls/select-runs-controls.component.html + 47 + Save @@ -7547,7 +7551,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html - 124 + 121 @@ -7586,7 +7590,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/app/teacher/teacher-run-list/teacher-run-list.component.html - 35 + 31 @@ -7604,7 +7608,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/app/teacher/teacher-run-list/teacher-run-list.component.html - 50 + 46 @@ -8231,8 +8235,8 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.34 - src/app/teacher/teacher-run-list/teacher-run-list.component.html - 87 + src/app/teacher/select-runs-controls/select-runs-controls.component.html + 35 @@ -8249,32 +8253,51 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.78 - - Successfully archived unit. + + Successfully unit. src/app/teacher/run-menu/run-menu.component.ts - 92 + 104 - - Error archiving unit. + + Error unit. src/app/teacher/run-menu/run-menu.component.ts - 93 + 110 - - Successfully restored unit. + + Undo src/app/teacher/run-menu/run-menu.component.ts - 101 + 120 + + + src/app/teacher/select-runs-controls/select-runs-controls.component.ts + 92 - - Error restoring unit. + + Action undone. src/app/teacher/run-menu/run-menu.component.ts - 102 + 132 + + + src/app/teacher/select-runs-controls/select-runs-controls.component.ts + 111 + + + + Error undoing action. + + src/app/teacher/run-menu/run-menu.component.ts + 135 + + + src/app/teacher/select-runs-controls/select-runs-controls.component.ts + 114 @@ -8528,18 +8551,11 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.18 - - Running - - src/app/teacher/select-runs-controls/select-runs-controls.component.html - 23 - - Completed src/app/teacher/select-runs-controls/select-runs-controls.component.html - 24 + 23 src/assets/wise5/classroomMonitor/classroomMonitorComponents/milestones/milestone-details/milestone-details.component.html @@ -8550,6 +8566,69 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.20 + + Running + + src/app/teacher/select-runs-controls/select-runs-controls.component.html + 24 + + + + Scheduled + + src/app/teacher/select-runs-controls/select-runs-controls.component.html + 25 + + + + selected + + src/app/teacher/select-runs-controls/select-runs-controls.component.html + 28 + + + + Archive selected units + + src/app/teacher/select-runs-controls/select-runs-controls.component.html + 33 + + + + Restore selected units + + src/app/teacher/select-runs-controls/select-runs-controls.component.html + 45 + + + + Successfully archived unit(s). + + src/app/teacher/select-runs-controls/select-runs-controls.component.ts + 90 + + + + Successfully restored unit(s). + + src/app/teacher/select-runs-controls/select-runs-controls.component.ts + 91 + + + + Error archiving unit(s). + + src/app/teacher/select-runs-controls/select-runs-controls.component.ts + 102 + + + + Error restoring unit(s). + + src/app/teacher/select-runs-controls/select-runs-controls.component.ts + 102 + + Share with Students @@ -8793,21 +8872,21 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.(Legacy Unit) src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html - 91 + 88 Last student login: src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html - 121 + 118 Teacher Tools src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html - 132 + 129 @@ -8817,116 +8896,74 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.82 - - Hey there! Looks like you haven't run any WISE units in your classes yet. - - src/app/teacher/teacher-run-list/teacher-run-list.component.html - 2 - - - - Browse the "Unit Library" to find titles to use with your students. - - src/app/teacher/teacher-run-list/teacher-run-list.component.html - 3 - - Active src/app/teacher/teacher-run-list/teacher-run-list.component.html - 27 + 23 Archived src/app/teacher/teacher-run-list/teacher-run-list.component.html - 28 + 24 Archived classroom units: src/app/teacher/teacher-run-list/teacher-run-list.component.html - 38 + 34 Active classroom units: src/app/teacher/teacher-run-list/teacher-run-list.component.html - 39 + 35 completed src/app/teacher/teacher-run-list/teacher-run-list.component.html - 44 + 40 running src/app/teacher/teacher-run-list/teacher-run-list.component.html - 47 + 43 Clear filters src/app/teacher/teacher-run-list/teacher-run-list.component.html - 61 + 57 - - selected + + Hey there! Looks like you don't have any active classroom units. src/app/teacher/teacher-run-list/teacher-run-list.component.html - 78,80 + 65 - - Archive selected units + + Browse the "Unit Library" to find titles to use with your students. src/app/teacher/teacher-run-list/teacher-run-list.component.html - 85 + 66 - - Restore selected units + + Looks like you don't have any archived classroom units. src/app/teacher/teacher-run-list/teacher-run-list.component.html - 97 - - - - Successfully archived unit(s). - - src/app/teacher/teacher-run-list/teacher-run-list.component.ts - 249,251 - - - - Error archiving unit(s). - - src/app/teacher/teacher-run-list/teacher-run-list.component.ts - 255 - - - - Successfully restored unit(s). - - src/app/teacher/teacher-run-list/teacher-run-list.component.ts - 267,269 - - - - Error restoring unit(s). - - src/app/teacher/teacher-run-list/teacher-run-list.component.ts - 273 + 69 From 0287de660ca12ef74fe1c4cd6312656b9bc6d60a Mon Sep 17 00:00:00 2001 From: Geoffrey Kwan Date: Wed, 30 Aug 2023 17:53:03 -0400 Subject: [PATCH 38/43] test(Archive Run): Fix teacher-run-list-item test #1012 --- src/app/teacher/run-menu/run-menu.harness.ts | 21 +++++++++++++++++-- .../teacher-run-list-item.component.spec.ts | 5 +++-- .../teacher-run-list-item.harness.ts | 2 +- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/app/teacher/run-menu/run-menu.harness.ts b/src/app/teacher/run-menu/run-menu.harness.ts index abc95898c91..dd30bb97bda 100644 --- a/src/app/teacher/run-menu/run-menu.harness.ts +++ b/src/app/teacher/run-menu/run-menu.harness.ts @@ -1,13 +1,30 @@ import { ComponentHarness } from '@angular/cdk/testing'; import { clickMenuButton } from '../../common/harness-helper'; +import { MatMenuHarness } from '@angular/material/menu/testing'; export class RunMenuHarness extends ComponentHarness { static hostSelector = 'app-run-menu'; + private ARCHIVE_MENU_BUTTON_TEXT = 'archiveArchive'; + private UNARCHIVE_MENU_BUTTON_TEXT = 'unarchiveRestore'; + async clickArchiveMenuButton(): Promise { - return await clickMenuButton(this, 'archiveArchive'); + return await clickMenuButton(this, this.ARCHIVE_MENU_BUTTON_TEXT); } async clickUnarchiveMenuButton(): Promise { - return await clickMenuButton(this, 'unarchiveRestore'); + return await clickMenuButton(this, this.UNARCHIVE_MENU_BUTTON_TEXT); + } + + async hasRestoreMenuButton(): Promise { + const getMenu = this.locatorFor(MatMenuHarness); + const menu = await getMenu(); + await menu.open(); + let foundRestoreMenuButton = false; + for (const item of await menu.getItems()) { + if ((await item.getText()) === this.UNARCHIVE_MENU_BUTTON_TEXT) { + foundRestoreMenuButton = true; + } + } + return foundRestoreMenuButton; } } diff --git a/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.spec.ts b/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.spec.ts index 025dfe6f081..d45f1881adc 100644 --- a/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.spec.ts +++ b/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.spec.ts @@ -112,10 +112,11 @@ function render() { } function runArchiveStatusChanged() { - describe('runArchiveStatusChanged()', () => { - it('should unselect run and emit events', async () => { + describe('run is not archived and archive menu button is clicked', () => { + it('should archive run and emit events', async () => { const runSelectedSpy = spyOn(component.runSelectedStatusChangedEvent, 'emit'); const runArchiveSpy = spyOn(component.runArchiveStatusChangedEvent, 'emit'); + expect(await runListItemHarness.isArchived()).toBeFalse(); await runListItemHarness.clickArchiveMenuButton(); expect(await runListItemHarness.isArchived()).toBeTrue(); expect(runSelectedSpy).toHaveBeenCalled(); diff --git a/src/app/teacher/teacher-run-list-item/teacher-run-list-item.harness.ts b/src/app/teacher/teacher-run-list-item/teacher-run-list-item.harness.ts index 7fad3371c8a..a45d77aa65d 100644 --- a/src/app/teacher/teacher-run-list-item/teacher-run-list-item.harness.ts +++ b/src/app/teacher/teacher-run-list-item/teacher-run-list-item.harness.ts @@ -30,6 +30,6 @@ export class TeacherRunListItemHarness extends ComponentHarness { } async isArchived(): Promise { - return (await this.locatorForOptional('.mat-mdc-card-title.warn')()) != null; + return (await this.getMenu()).hasRestoreMenuButton(); } } From f260318dbe3dbf8bdf5521fa086f35a19b5a6e7d Mon Sep 17 00:00:00 2001 From: Geoffrey Kwan Date: Thu, 31 Aug 2023 15:44:18 -0400 Subject: [PATCH 39/43] feat(Archive Run): Use select runs option enum #1012 --- .../select-runs-controls.component.html | 10 ++++---- .../select-runs-controls.component.ts | 7 +++--- .../select-runs-option.ts | 7 ++++++ .../teacher-run-list.component.ts | 25 +++---------------- src/app/teacher/teacher-run.ts | 21 ++++++++++++++++ 5 files changed, 41 insertions(+), 29 deletions(-) create mode 100644 src/app/teacher/select-runs-controls/select-runs-option.ts diff --git a/src/app/teacher/select-runs-controls/select-runs-controls.component.html b/src/app/teacher/select-runs-controls/select-runs-controls.component.html index 61b6fcaa62a..7409531d769 100644 --- a/src/app/teacher/select-runs-controls/select-runs-controls.component.html +++ b/src/app/teacher/select-runs-controls/select-runs-controls.component.html @@ -18,11 +18,11 @@ arrow_drop_down - - - - - + + + + +
{{ numSelectedRuns }} selected
diff --git a/src/app/teacher/select-runs-controls/select-runs-controls.component.ts b/src/app/teacher/select-runs-controls/select-runs-controls.component.ts index 9724571df3c..5c722a29b54 100644 --- a/src/app/teacher/select-runs-controls/select-runs-controls.component.ts +++ b/src/app/teacher/select-runs-controls/select-runs-controls.component.ts @@ -6,6 +6,7 @@ import { Subscription } from 'rxjs'; import { Project } from '../../domain/project'; import { ArchiveProjectService } from '../../services/archive-project.service'; import { MatSnackBar } from '@angular/material/snack-bar'; +import { SelectRunsOption } from './select-runs-option'; @Component({ selector: 'select-runs-controls', @@ -20,7 +21,7 @@ export class SelectRunsControlsComponent { @Input() runs: TeacherRun[] = []; protected selectedAllRuns: boolean = false; protected selectedSomeRuns: boolean = false; - @Output() selectRunsOptionChosenEvent = new EventEmitter(); + @Output() selectRunsOptionChosenEvent = new EventEmitter(); @Input() showArchived: boolean = false; constructor( @@ -42,12 +43,12 @@ export class SelectRunsControlsComponent { protected selectAllRunsCheckboxClicked(): void { this.selectRunsOptionChosenEvent.emit( - this.selectedAllRuns || this.selectedSomeRuns ? 'none' : 'all' + this.selectedAllRuns || this.selectedSomeRuns ? SelectRunsOption.None : SelectRunsOption.All ); } protected selectRunsOptionChosen(value: string): void { - this.selectRunsOptionChosenEvent.emit(value); + this.selectRunsOptionChosenEvent.emit(value as SelectRunsOption); } protected archiveSelectedRuns(archive: boolean): Subscription { diff --git a/src/app/teacher/select-runs-controls/select-runs-option.ts b/src/app/teacher/select-runs-controls/select-runs-option.ts new file mode 100644 index 00000000000..a64017ff589 --- /dev/null +++ b/src/app/teacher/select-runs-controls/select-runs-option.ts @@ -0,0 +1,7 @@ +export enum SelectRunsOption { + All = 'ALL', + None = 'NONE', + Completed = 'COMPLETED', + Running = 'RUNNING', + Scheduled = 'SCHEDULED' +} diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.ts b/src/app/teacher/teacher-run-list/teacher-run-list.component.ts index cee5f2d5397..d498f3e5888 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.ts +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.ts @@ -8,6 +8,7 @@ import { UserService } from '../../services/user.service'; import { mergeMap } from 'rxjs/operators'; import { ArchiveProjectService } from '../../services/archive-project.service'; import { runSpansDays } from '../../../assets/wise5/common/datetime/datetime'; +import { SelectRunsOption } from '../select-runs-controls/select-runs-option'; @Component({ selector: 'app-teacher-run-list', @@ -210,27 +211,9 @@ export class TeacherRunListComponent implements OnInit { run.selected = false; } } - - protected selectRunsOptionChosen(value: string): void { - this.filteredRuns.forEach((run: TeacherRun) => { - switch (value) { - case 'all': - run.selected = true; - break; - case 'none': - run.selected = false; - break; - case 'completed': - run.selected = run.isCompleted(this.configService.getCurrentServerTime()); - break; - case 'running': - run.selected = run.isActive(this.configService.getCurrentServerTime()); - break; - case 'scheduled': - run.selected = run.isScheduled(this.configService.getCurrentServerTime()); - break; - } - }); + protected selectRunsOptionChosen(option: SelectRunsOption): void { + const now = this.configService.getCurrentServerTime(); + this.filteredRuns.forEach((run: TeacherRun) => run.updateSelected(option, now)); this.runSelectedStatusChanged(); } diff --git a/src/app/teacher/teacher-run.ts b/src/app/teacher/teacher-run.ts index aa94c53ebdd..7678ba8c4c7 100644 --- a/src/app/teacher/teacher-run.ts +++ b/src/app/teacher/teacher-run.ts @@ -1,4 +1,5 @@ import { Run } from '../domain/run'; +import { SelectRunsOption } from './select-runs-controls/select-runs-option'; export class TeacherRun extends Run { archived: boolean; @@ -9,4 +10,24 @@ export class TeacherRun extends Run { constructor(jsonObject: any = {}) { super(jsonObject); } + + updateSelected(selectRunsOption: SelectRunsOption, currentTime: number): void { + switch (selectRunsOption) { + case SelectRunsOption.All: + this.selected = true; + break; + case SelectRunsOption.None: + this.selected = false; + break; + case SelectRunsOption.Completed: + this.selected = this.isCompleted(currentTime); + break; + case SelectRunsOption.Running: + this.selected = this.isActive(currentTime); + break; + case SelectRunsOption.Scheduled: + this.selected = this.isScheduled(currentTime); + break; + } + } } From 0a905b37c95d61bf1826a370f2fe5005f17549fa Mon Sep 17 00:00:00 2001 From: Geoffrey Kwan Date: Sun, 3 Sep 2023 12:04:28 -0400 Subject: [PATCH 40/43] test(Archive Run): Clean up test and add missing new line #1012 --- .../teacher-run-list.component.spec.ts | 59 ++++++------------- .../teacher-run-list.component.ts | 1 + 2 files changed, 18 insertions(+), 42 deletions(-) diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts b/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts index 32460adc2f6..5860f4d78b9 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts @@ -35,13 +35,13 @@ let configService: ConfigService; const currentTime = new Date().getTime(); let fixture: ComponentFixture; let getRunsSpy: jasmine.Spy; -let runListHarness: TeacherRunListHarness; const run1StartTime = new Date('2020-01-01').getTime(); -const run2StartTime = new Date('2020-01-02').getTime(); -const run3StartTime = new Date('2020-01-03').getTime(); const run1Title = 'First Run'; +const run2StartTime = new Date('2020-01-02').getTime(); const run2Title = 'Second Run'; +const run3StartTime = new Date('2020-01-03').getTime(); const run3Title = 'Third Run'; +let runListHarness: TeacherRunListHarness; let teacherService: TeacherService; const userId: number = 1; let userService: UserService; @@ -109,9 +109,9 @@ describe('TeacherRunListComponent', () => { getRunsSpy = spyOn(teacherService, 'getRuns'); getRunsSpy.and.returnValue( of([ - new TeacherRunStub(1, run1StartTime, null, run1Title), + new TeacherRunStub(1, run1StartTime, currentTime - 1000, run1Title), new TeacherRunStub(2, run2StartTime, null, run2Title), - new TeacherRunStub(3, run3StartTime, null, run3Title) + new TeacherRunStub(3, currentTime + 86400000, null, run3Title) ]) ); spyOn(configService, 'getCurrentServerTime').and.returnValue(currentTime); @@ -279,43 +279,40 @@ function selectRunsOptionChosen(): void { } function selectAllOptionChosen(): void { - describe('when all is chosen', () => { - it('should select all runs', async () => { + describe('when all option is chosen', () => { + it('should select all runs checkboxes', async () => { await clickSelectRunsMenuButtonAndExpectSelected('All', [true, true, true]); }); }); } function selectNoneOptionChosen(): void { - describe('when none is chosen', () => { - it('should select no runs', async () => { + describe('when none option is chosen', () => { + it('should select no runs checkboxes', async () => { await clickSelectRunsMenuButtonAndExpectSelected('None', [false, false, false]); }); }); } function selectCompletedOptionChosen(): void { - describe('when completed is chosen', () => { - it('should select completed runs', async () => { - setRun2Completed(); - await clickSelectRunsMenuButtonAndExpectSelected('Completed', [false, true, false]); + describe('when completed option is chosen', () => { + it('should select completed runs checkboxes', async () => { + await clickSelectRunsMenuButtonAndExpectSelected('Completed', [false, false, true]); }); }); } function selectRunningOptionChosen(): void { - describe('when running is chosen', () => { - it('should select running runs', async () => { - setRun2Completed(); - await clickSelectRunsMenuButtonAndExpectSelected('Running', [true, false, true]); + describe('when running option is chosen', () => { + it('should select running runs checkboxes', async () => { + await clickSelectRunsMenuButtonAndExpectSelected('Running', [false, true, false]); }); }); } function selectScheduledOptionChosen(): void { - describe('when scheduled is chosen', () => { - it('it should select scheduled runs', async () => { - setRun3Scheduled(); + describe('when scheduled option is chosen', () => { + it('should select scheduled runs checkboxes', async () => { await clickSelectRunsMenuButtonAndExpectSelected('Scheduled', [true, false, false]); }); }); @@ -329,28 +326,6 @@ async function clickSelectRunsMenuButtonAndExpectSelected( await expectRunsIsSelected(selectedRuns); } -function setRun2Completed(): void { - getRunsSpy.and.returnValue( - of([ - new TeacherRunStub(1, run1StartTime, null, run1Title), - new TeacherRunStub(2, run2StartTime, currentTime - 1000, run2Title), - new TeacherRunStub(3, run3StartTime, null, run3Title) - ]) - ); - component.ngOnInit(); -} - -function setRun3Scheduled(): void { - getRunsSpy.and.returnValue( - of([ - new TeacherRunStub(1, run1StartTime, null, run1Title), - new TeacherRunStub(2, run2StartTime, null, run2Title), - new TeacherRunStub(3, currentTime + 86400000, null, run3Title) - ]) - ); - component.ngOnInit(); -} - function runArchiveStatusChanged(): void { describe('runArchiveStatusChanged()', () => { archiveRunNoLongerInActiveView(); diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.ts b/src/app/teacher/teacher-run-list/teacher-run-list.component.ts index d498f3e5888..2236a7aee84 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.ts +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.ts @@ -211,6 +211,7 @@ export class TeacherRunListComponent implements OnInit { run.selected = false; } } + protected selectRunsOptionChosen(option: SelectRunsOption): void { const now = this.configService.getCurrentServerTime(); this.filteredRuns.forEach((run: TeacherRun) => run.updateSelected(option, now)); From 67725617e4c5af53b7c1a2430a512309d2e9a678 Mon Sep 17 00:00:00 2001 From: Geoffrey Kwan Date: Wed, 6 Sep 2023 18:09:36 -0400 Subject: [PATCH 41/43] test(Archive Run): Clean up test #1012 --- .../teacher-run-list.component.spec.ts | 68 +++++-------------- 1 file changed, 16 insertions(+), 52 deletions(-) diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts b/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts index 5860f4d78b9..c5a8dd0e23a 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts @@ -270,62 +270,26 @@ function someSelectedUnselectAllRuns() { function selectRunsOptionChosen(): void { describe('selectRunsOptionChosen()', () => { - selectAllOptionChosen(); - selectNoneOptionChosen(); - selectCompletedOptionChosen(); - selectRunningOptionChosen(); - selectScheduledOptionChosen(); - }); -} - -function selectAllOptionChosen(): void { - describe('when all option is chosen', () => { - it('should select all runs checkboxes', async () => { - await clickSelectRunsMenuButtonAndExpectSelected('All', [true, true, true]); - }); - }); -} - -function selectNoneOptionChosen(): void { - describe('when none option is chosen', () => { - it('should select no runs checkboxes', async () => { - await clickSelectRunsMenuButtonAndExpectSelected('None', [false, false, false]); - }); - }); -} - -function selectCompletedOptionChosen(): void { - describe('when completed option is chosen', () => { - it('should select completed runs checkboxes', async () => { - await clickSelectRunsMenuButtonAndExpectSelected('Completed', [false, false, true]); - }); - }); -} - -function selectRunningOptionChosen(): void { - describe('when running option is chosen', () => { - it('should select running runs checkboxes', async () => { - await clickSelectRunsMenuButtonAndExpectSelected('Running', [false, true, false]); - }); - }); -} - -function selectScheduledOptionChosen(): void { - describe('when scheduled option is chosen', () => { - it('should select scheduled runs checkboxes', async () => { - await clickSelectRunsMenuButtonAndExpectSelected('Scheduled', [true, false, false]); + const testCases = [ + { menuButtonText: 'All', selectedRuns: [true, true, true] }, + { menuButtonText: 'None', selectedRuns: [false, false, false] }, + { menuButtonText: 'Completed', selectedRuns: [false, false, true] }, + { menuButtonText: 'Running', selectedRuns: [false, true, false] }, + { menuButtonText: 'Scheduled', selectedRuns: [true, false, false] } + ]; + testCases.forEach(({ menuButtonText, selectedRuns }) => { + describe(`when ${menuButtonText} option is chosen`, () => { + beforeEach(async () => { + await runListHarness.clickSelectRunsMenuButton(menuButtonText); + }); + it(`should select ${menuButtonText} runs`, async () => { + await expectRunsIsSelected(selectedRuns); + }); + }); }); }); } -async function clickSelectRunsMenuButtonAndExpectSelected( - menuButtonText: string, - selectedRuns: boolean[] -): Promise { - await runListHarness.clickSelectRunsMenuButton(menuButtonText); - await expectRunsIsSelected(selectedRuns); -} - function runArchiveStatusChanged(): void { describe('runArchiveStatusChanged()', () => { archiveRunNoLongerInActiveView(); From 83d4924d532d05c635654af419d25580bc456fe6 Mon Sep 17 00:00:00 2001 From: Geoffrey Kwan Date: Tue, 12 Sep 2023 15:32:45 -0400 Subject: [PATCH 42/43] fix(Archive Run): Display first 10 runs immediately #1012 --- src/app/teacher/teacher-run-list/teacher-run-list.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.ts b/src/app/teacher/teacher-run-list/teacher-run-list.component.ts index 2236a7aee84..d3ef6ae202f 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.ts +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.ts @@ -57,7 +57,6 @@ export class TeacherRunListComponent implements OnInit { this.setRuns(runs); this.processRuns(); this.highlightNewRunIfNecessary(); - this.loaded = true; }); } @@ -79,6 +78,7 @@ export class TeacherRunListComponent implements OnInit { return teacherRun; }); this.filteredRuns = this.runs; + this.loaded = true; } private subscribeToRuns(): void { From 71c635a2387af9a0c1a08e1408bde81392c04de9 Mon Sep 17 00:00:00 2001 From: Geoffrey Kwan Date: Mon, 18 Sep 2023 15:52:46 -0400 Subject: [PATCH 43/43] feat(Archive Run): Only show number of runs after all runs are loaded #1012 --- .../teacher-run-list/teacher-run-list.component.html | 6 +++--- .../teacher/teacher-run-list/teacher-run-list.component.ts | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.html b/src/app/teacher/teacher-run-list/teacher-run-list.component.html index d67cbd6c0e8..6036c1273e9 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.html +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.html @@ -12,7 +12,7 @@ fxFlex.sm="0 0 calc(50%-8px)" i18n-placeholderText placeholderText="Search" - [disable]="!loaded" + [disable]="!recentRunsLoaded" [value]="searchValue" (update)="searchChanged($event)" > @@ -26,7 +26,7 @@
-

+

Units found: {{ filteredRuns.length }} @@ -59,7 +59,7 @@

- +

Hey there! Looks like you don't have any active classroom units.

diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.ts b/src/app/teacher/teacher-run-list/teacher-run-list.component.ts index d3ef6ae202f..184a83936d6 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.ts +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.ts @@ -18,10 +18,11 @@ import { SelectRunsOption } from '../select-runs-controls/select-runs-option'; export class TeacherRunListComponent implements OnInit { private MAX_RECENT_RUNS = 10; + protected allRunsLoaded: boolean = false; protected filteredRuns: TeacherRun[] = []; protected filterValue: string = ''; - protected loaded: boolean = false; protected numSelectedRuns: number = 0; + protected recentRunsLoaded: boolean = false; protected runChangedEventEmitter: EventEmitter = new EventEmitter(); protected runs: TeacherRun[] = []; protected searchValue: string = ''; @@ -57,6 +58,7 @@ export class TeacherRunListComponent implements OnInit { this.setRuns(runs); this.processRuns(); this.highlightNewRunIfNecessary(); + this.allRunsLoaded = true; }); } @@ -78,7 +80,7 @@ export class TeacherRunListComponent implements OnInit { return teacherRun; }); this.filteredRuns = this.runs; - this.loaded = true; + this.recentRunsLoaded = true; } private subscribeToRuns(): void {