diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 7646f44884c..d67aecf7795 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -1,9 +1,9 @@ +import { FormsModule } from '@angular/forms'; import { HTTP_INTERCEPTORS, HttpRequest, HttpHandler, HttpInterceptor } from '@angular/common/http'; import { Injectable, NgModule } from '@angular/core'; -import { RouterModule, Routes } from '@angular/router'; -import { FormsModule } from '@angular/forms'; -import { PublicLibraryComponent } from './modules/library/public-library/public-library.component'; import { PersonalLibraryComponent } from './modules/library/personal-library/personal-library.component'; +import { PublicLibraryComponent } from './modules/library/public-library/public-library.component'; +import { RouterModule, Routes } from '@angular/router'; const routes: Routes = [ { path: '', loadChildren: () => import('./home/home.module').then((m) => m.HomeModule) }, @@ -56,6 +56,10 @@ const routes: Routes = [ { path: 'teacher', loadChildren: () => import('./teacher/teacher.module').then((m) => m.TeacherModule) + }, + { + path: 'survey', + loadChildren: () => import('./student/survey/survey.module').then((m) => m.SurveyModule) } ]; diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 808b00ed04f..5f5c39966c0 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -190,7 +190,8 @@ export class AppComponent { !this.router.url.includes('/login') && !this.router.url.includes('/join') && !this.router.url.includes('/contact') && - !this.router.url.includes('/forgot') + !this.router.url.includes('/forgot') && + !this.router.url.includes('/survey') ); } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 8deefe3cc8e..0c1746e26da 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,29 +1,30 @@ -import { NgModule, inject, provideAppInitializer } from '@angular/core'; +import { AnnouncementComponent } from './announcement/announcement.component'; +import { AnnouncementDialogComponent } from './announcement/announcement.component'; +import { AppComponent } from './app.component'; +import { AppRoutingModule } from './app-routing.module'; +import { ArchiveProjectService } from './services/archive-project.service'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserModule } from '@angular/platform-browser'; +import { ConfigService } from './services/config.service'; +import { FooterComponent } from './modules/footer/footer.component'; import { FormsModule } from '@angular/forms'; -import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; +import { HeaderComponent } from './modules/header/header.component'; +import { HomeModule } from './home/home.module'; import { HttpErrorInterceptor } from './http-error.interceptor'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { RouterModule } from '@angular/router'; +import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; +import { LogOutService } from './services/logOutService'; import { MatDialogModule } from '@angular/material/dialog'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MAT_SNACK_BAR_DEFAULT_OPTIONS } from '@angular/material/snack-bar'; -import { AppComponent } from './app.component'; -import { AppRoutingModule } from './app-routing.module'; -import { ConfigService } from './services/config.service'; -import { HomeModule } from './home/home.module'; +import { MobileMenuComponent } from './modules/mobile-menu/mobile-menu.component'; +import { NgModule, inject, provideAppInitializer } from '@angular/core'; +import { RecaptchaV3Module, RECAPTCHA_V3_SITE_KEY, RECAPTCHA_BASE_URL } from 'ng-recaptcha-2'; +import { RouterModule } from '@angular/router'; import { StudentService } from './student/student.service'; -import { UserService } from './services/user.service'; import { TeacherService } from './teacher/teacher.service'; -import { MobileMenuComponent } from './modules/mobile-menu/mobile-menu.component'; -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-2'; -import { ArchiveProjectService } from './services/archive-project.service'; -import { FooterComponent } from './modules/footer/footer.component'; -import { HeaderComponent } from './modules/header/header.component'; +import { UserService } from './services/user.service'; export function initialize( configService: ConfigService, @@ -47,17 +48,17 @@ export function initialize( bootstrap: [AppComponent], imports: [ AnnouncementComponent, - BrowserModule, - BrowserAnimationsModule, - FormsModule, AppRoutingModule, + BrowserAnimationsModule, + BrowserModule, FooterComponent, + FormsModule, HeaderComponent, HomeModule, - MobileMenuComponent, + MatDialogModule, MatSidenavModule, MatSnackBarModule, - MatDialogModule, + MobileMenuComponent, RecaptchaV3Module, RouterModule.forRoot([], { scrollPositionRestoration: 'enabled', @@ -69,13 +70,14 @@ export function initialize( providers: [ ArchiveProjectService, ConfigService, + LogOutService, StudentService, TeacherService, UserService, provideAppInitializer(() => { - const initializerFn = (initialize)(inject(ConfigService), inject(UserService)); - return initializerFn(); - }), + const initializerFn = initialize(inject(ConfigService), inject(UserService)); + return initializerFn(); + }), { provide: MAT_SNACK_BAR_DEFAULT_OPTIONS, useValue: { diff --git a/src/app/domain/run.ts b/src/app/domain/run.ts index 51960909f40..86e1d56704e 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; + private isSurvey: boolean; static readonly VIEW_STUDENT_WORK_PERMISSION: number = 1; static readonly GRADE_AND_MANAGE_PERMISSION: number = 2; @@ -39,32 +40,32 @@ export class Run { } } - public canViewStudentWork(userId) { + public canViewStudentWork(userId): boolean { return ( this.isOwner(userId) || this.isSharedOwnerWithPermission(userId, Run.VIEW_STUDENT_WORK_PERMISSION) ); } - public canGradeAndManage(userId) { + public canGradeAndManage(userId): boolean { return ( this.isOwner(userId) || this.isSharedOwnerWithPermission(userId, Run.GRADE_AND_MANAGE_PERMISSION) ); } - public canViewStudentNames(userId) { + public canViewStudentNames(userId): boolean { return ( this.isOwner(userId) || this.isSharedOwnerWithPermission(userId, Run.VIEW_STUDENT_NAMES_PERMISSION) ); } - isOwner(userId) { + isOwner(userId): boolean { return this.owner.id == userId; } - isSharedOwnerWithPermission(userId, permissionId) { + isSharedOwnerWithPermission(userId, permissionId): boolean { for (const sharedOwner of this.sharedOwners) { if (sharedOwner.id == userId) { return this.userHasPermission(sharedOwner, permissionId); @@ -73,7 +74,7 @@ export class Run { return false; } - userHasPermission(user: User, permission: number) { + userHasPermission(user: User, permission: number): boolean { return user.permissions.includes(permission); } @@ -92,6 +93,10 @@ export class Run { private hasEndTime(): boolean { return this.endTime != null; } + + isSurveyRun(): boolean { + return this.isSurvey; + } } export function sortByRunStartTimeDesc(a: Run, b: Run): number { diff --git a/src/app/modules/header/header-account-menu/header-account-menu.component.html b/src/app/modules/header/header-account-menu/header-account-menu.component.html index 541a6e0bc16..db4550b0f1e 100644 --- a/src/app/modules/header/header-account-menu/header-account-menu.component.html +++ b/src/app/modules/header/header-account-menu/header-account-menu.component.html @@ -6,80 +6,82 @@
- + + + - - account_box - - {{ firstName }} {{ lastName }} -
-
- Switch back to original user -
- - - home - Student Home - - - home - Teacher Home - - - edit - Edit Profile - - - edit - Edit Profile - - - settings - Researcher Tools - - - settings - Admin Tools - - - help - Help - - + home + Student Home + + + home + Teacher Home + + + edit + Edit Profile + + + edit + Edit Profile + + + settings + Researcher Tools + + + settings + Admin Tools + + + help + Help + + + } exit_to_app Sign Out diff --git a/src/app/modules/header/header-account-menu/header-account-menu.component.spec.ts b/src/app/modules/header/header-account-menu/header-account-menu.component.spec.ts index 642a3db0a80..75417aac6dd 100644 --- a/src/app/modules/header/header-account-menu/header-account-menu.component.spec.ts +++ b/src/app/modules/header/header-account-menu/header-account-menu.component.spec.ts @@ -1,13 +1,14 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { Config } from '../../../domain/config'; +import { ConfigService } from '../../../services/config.service'; import { HeaderAccountMenuComponent } from './header-account-menu.component'; -import { User } from '../../../domain/user'; import { MatMenuModule } from '@angular/material/menu'; -import { ConfigService } from '../../../services/config.service'; import { Observable } from 'rxjs'; +import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { provideHttpClientTesting } from '@angular/common/http/testing'; -import { Config } from '../../../domain/config'; import { provideRouter } from '@angular/router'; -import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; +import { User } from '../../../domain/user'; +import { LogOutService } from '../../../services/logOutService'; export class MockConfigService { getConfig(): Observable { @@ -27,14 +28,18 @@ describe('HeaderAccountMenuComponent', () => { let component: HeaderAccountMenuComponent; let fixture: ComponentFixture; - beforeEach( - waitForAsync(() => { - TestBed.configureTestingModule({ - imports: [HeaderAccountMenuComponent, MatMenuModule], - providers: [{ provide: ConfigService, useClass: MockConfigService }, provideRouter([]), provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()] -}).compileComponents(); - }) - ); + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [HeaderAccountMenuComponent, MatMenuModule], + providers: [ + LogOutService, + { provide: ConfigService, useClass: MockConfigService }, + provideRouter([]), + provideHttpClient(withInterceptorsFromDi()), + provideHttpClientTesting() + ] + }).compileComponents(); + })); beforeEach(() => { fixture = TestBed.createComponent(HeaderAccountMenuComponent); diff --git a/src/app/modules/header/header-account-menu/header-account-menu.component.ts b/src/app/modules/header/header-account-menu/header-account-menu.component.ts index 652c3bde686..dc0737296a9 100644 --- a/src/app/modules/header/header-account-menu/header-account-menu.component.ts +++ b/src/app/modules/header/header-account-menu/header-account-menu.component.ts @@ -1,45 +1,41 @@ -import { Component, OnInit, Input, SimpleChanges } from '@angular/core'; -import { ConfigService } from '../../../services/config.service'; -import { User } from '../../../domain/user'; +import { CommonModule } from '@angular/common'; +import { Component, Input, SimpleChanges } from '@angular/core'; +import { FlexLayoutModule } from '@angular/flex-layout'; import { HttpClient } from '@angular/common/http'; +import { LogOutService } from '../../../services/logOutService'; import { MatButtonModule } from '@angular/material/button'; +import { MatDividerModule } from '@angular/material/divider'; import { MatIconModule } from '@angular/material/icon'; import { MatMenuModule } from '@angular/material/menu'; -import { MatDividerModule } from '@angular/material/divider'; import { RouterModule } from '@angular/router'; -import { CommonModule } from '@angular/common'; -import { FlexLayoutModule } from '@angular/flex-layout'; +import { User } from '../../../domain/user'; @Component({ - selector: 'app-header-account-menu', - templateUrl: './header-account-menu.component.html', - styleUrl: './header-account-menu.component.scss', - imports: [ - CommonModule, - FlexLayoutModule, - MatButtonModule, - MatIconModule, - MatMenuModule, - MatDividerModule, - RouterModule - ] + selector: 'app-header-account-menu', + templateUrl: './header-account-menu.component.html', + styleUrl: './header-account-menu.component.scss', + imports: [ + CommonModule, + FlexLayoutModule, + MatButtonModule, + MatDividerModule, + MatIconModule, + MatMenuModule, + RouterModule + ] }) -export class HeaderAccountMenuComponent implements OnInit { +export class HeaderAccountMenuComponent { protected firstName: string = ''; protected isPreviousAdmin: boolean; protected lastName: string = ''; - protected logOutURL: string; protected roles: string[] = []; private switchToOriginalUserURL = '/api/logout/impersonate'; @Input() user: User; - constructor(private configService: ConfigService, private http: HttpClient) {} - - ngOnInit(): void { - this.configService.getConfig().subscribe((config) => { - this.logOutURL = config.logOutURL; - }); - } + constructor( + private http: HttpClient, + private logOutService: LogOutService + ) {} ngOnChanges(changes: SimpleChanges): void { if (changes.user) { @@ -64,8 +60,6 @@ export class HeaderAccountMenuComponent implements OnInit { } protected logOut(): void { - this.http.get(this.logOutURL).subscribe(() => { - window.location.href = '/'; - }); + this.logOutService.logOut(); } } diff --git a/src/app/services/accessLinkService.spec.ts b/src/app/services/accessLinkService.spec.ts new file mode 100644 index 00000000000..ba46165b344 --- /dev/null +++ b/src/app/services/accessLinkService.spec.ts @@ -0,0 +1,33 @@ +import { AccessLinkService } from './accessLinkService'; +import { ConfigService } from './config.service'; +import { TestBed } from '@angular/core/testing'; +import { HttpClient } from '@angular/common/http'; +import { MockProviders } from 'ng-mocks'; + +let service: AccessLinkService; +export class MockConfigService {} +describe('AccessLinkService', () => { + const linkBase = 'wise.berkeley.edu/run-survey/dog1234-'; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [AccessLinkService, MockProviders(ConfigService, HttpClient)] + }); + service = TestBed.inject(AccessLinkService); + spyOn(TestBed.inject(ConfigService), 'getWISEHostname').and.returnValue('wise.berkeley.edu'); + spyOn(TestBed.inject(ConfigService), 'getContextPath').and.returnValue(''); + }); + + it('should get access links', () => { + const accessLinks = service.getAccessLinks('dog1234', ['1', 'test', 'this is a test']); + expect(accessLinks[0]).toEqual(linkBase + '1'); + expect(accessLinks[1]).toEqual(linkBase + 'test'); + expect(accessLinks[2]).toEqual(linkBase + 'this is a test'); + }); + + it('should get period from access link', () => { + expect(service.getPeriodFromAccessLink(linkBase + '1')).toEqual('1'); + expect(service.getPeriodFromAccessLink(linkBase + 'test')).toEqual('test'); + expect(service.getPeriodFromAccessLink(linkBase + 'this is a test')).toEqual('this is a test'); + }); +}); diff --git a/src/app/services/accessLinkService.ts b/src/app/services/accessLinkService.ts new file mode 100644 index 00000000000..219d08efbfe --- /dev/null +++ b/src/app/services/accessLinkService.ts @@ -0,0 +1,17 @@ +import { Injectable } from '@angular/core'; +import { ConfigService } from './config.service'; + +@Injectable() +export class AccessLinkService { + constructor(private configService: ConfigService) {} + + getAccessLinks(runCode: string, periods: string[]): string[] { + const host = this.configService.getWISEHostname() + this.configService.getContextPath(); + const linkBase = `${host}/run-survey/${runCode}-`; + return periods.map((period) => linkBase + period); + } + + getPeriodFromAccessLink(link: string): string { + return link.slice(link.lastIndexOf('-') + 1); + } +} diff --git a/src/app/services/logOutService.spec.ts b/src/app/services/logOutService.spec.ts new file mode 100644 index 00000000000..9d840d2c3fd --- /dev/null +++ b/src/app/services/logOutService.spec.ts @@ -0,0 +1,31 @@ +import { ConfigService } from './config.service'; +import { fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { HttpClient } from '@angular/common/http'; +import { LogOutService } from './logOutService'; +import { MockProviders } from 'ng-mocks'; +import { of } from 'rxjs'; + +let service: LogOutService; +let httpSpy: jasmine.Spy; +export class MockConfigService {} + +xdescribe('LogOutService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [LogOutService, MockProviders(ConfigService, HttpClient)] + }); + window.onbeforeunload = jasmine.createSpy(); + service = TestBed.inject(LogOutService); + httpSpy = spyOn(TestBed.inject(HttpClient), 'get').and.returnValue(of({})); + spyOn(TestBed.inject(ConfigService), 'getConfig').and.returnValue( + of({ contextPath: '', logOutURL: 'api/logOutUrl', currentTime: 0 }) + ); + spyOn(TestBed.inject(ConfigService), 'getContextPath').and.returnValue('wise.berkeley.edu'); + }); + + it('should make a GET request to the log out URL when logOut() is called', fakeAsync(() => { + service.logOut(); + tick(); + expect(httpSpy).toHaveBeenCalledWith('api/logOutUrl'); + })); +}); diff --git a/src/app/services/logOutService.ts b/src/app/services/logOutService.ts new file mode 100644 index 00000000000..f16bf204766 --- /dev/null +++ b/src/app/services/logOutService.ts @@ -0,0 +1,26 @@ +import { ConfigService } from './config.service'; +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; + +@Injectable() +export class LogOutService { + private logOutUrl: string; + + constructor( + private configService: ConfigService, + private http: HttpClient + ) {} + + async logOut(): Promise { + if (!this.logOutUrl) { + await this.retrieveLogOutUrl(); + } + this.http.get(this.logOutUrl).subscribe(); + } + + private async retrieveLogOutUrl(): Promise { + this.configService.getConfig().subscribe((config) => { + this.logOutUrl = config.logOutURL; + }); + } +} diff --git a/src/app/services/user.service.ts b/src/app/services/user.service.ts index 993ff48218d..d19e24ab221 100644 --- a/src/app/services/user.service.ts +++ b/src/app/services/user.service.ts @@ -1,12 +1,12 @@ -import { Injectable } from '@angular/core'; import { BehaviorSubject, Observable } from 'rxjs'; +import { ConfigService } from './config.service'; import { HttpClient, HttpHeaders } from '@angular/common/http'; -import { concatMap, tap } from 'rxjs/operators'; -import { User } from '../domain/user'; import { HttpParams } from '@angular/common/http'; -import { ConfigService } from './config.service'; -import { Teacher } from '../domain/teacher'; +import { Injectable } from '@angular/core'; import { Student } from '../domain/student'; +import { tap } from 'rxjs/operators'; +import { Teacher } from '../domain/teacher'; +import { User } from '../domain/user'; @Injectable() export class UserService { @@ -23,7 +23,10 @@ export class UserService { isAuthenticated = false; redirectUrl: string; // redirect here after logging in - constructor(private http: HttpClient, private configService: ConfigService) {} + constructor( + private configService: ConfigService, + private http: HttpClient + ) {} getUser(): BehaviorSubject { return this.user$; @@ -57,6 +60,10 @@ export class UserService { return this.isRole('admin'); } + isSurveyStudent(): boolean { + return this.isRole('surveyStudent'); + } + private isRole(role: string): boolean { return this.isAuthenticated && this.getRoles().includes(role); } @@ -75,16 +82,14 @@ export class UserService { retrieveUser(username?: string): Observable { const params = new HttpParams().set('username', username); - return this.http - .get(this.userUrl, { params: params }) - .pipe( - tap((user) => { - if (user != null && user.id != null) { - this.isAuthenticated = true; - } - this.user$.next(user); - }) - ); + return this.http.get(this.userUrl, { params: params }).pipe( + tap((user) => { + if (user != null && user.id != null) { + this.isAuthenticated = true; + } + this.user$.next(user); + }) + ); } checkAuthentication(username, password) { @@ -146,13 +151,11 @@ export class UserService { const headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded'); let body = new HttpParams(); body = body.set('newPassword', newPassword); - return this.http - .post(this.unlinkGoogleAccountUrl, body, { headers: headers }) - .pipe( - tap((user) => { - this.user$.next(user); - }) - ); + return this.http.post(this.unlinkGoogleAccountUrl, body, { headers: headers }).pipe( + tap((user) => { + this.user$.next(user); + }) + ); } getUserByGoogleId(googleUserId: string) { diff --git a/src/app/student/auth.guard.ts b/src/app/student/auth.guard.ts index af6190eebe0..de8ceca5cde 100644 --- a/src/app/student/auth.guard.ts +++ b/src/app/student/auth.guard.ts @@ -1,25 +1,37 @@ -import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router'; +import { Injectable } from '@angular/core'; import { UserService } from '../services/user.service'; @Injectable() export class AuthGuard { - constructor(private userService: UserService, private router: Router) {} + constructor( + private userService: UserService, + private router: Router + ) {} canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { return this.checkLogin(state.url); } - checkLogin(url: string): boolean { - if (this.userService.isStudent() || url.includes('/preview/unit')) { - return true; + private checkLogin(url: string): boolean { + let canAccessPage = false; + if (this.canAccess(url)) { + canAccessPage = true; } else if (this.userService.isAuthenticated) { this.router.navigate(['/']); - return false; } else { this.userService.redirectUrl = url; this.router.navigate(['/login']); - return false; } + return canAccessPage; + } + + private canAccess(url: string): boolean { + return ( + (this.userService.isStudent() || + url.includes('/preview/unit') || + url.includes('/workgroupLimitReached')) && + !(this.userService.isSurveyStudent() && url.includes('/home')) + ); } } diff --git a/src/app/student/run-info.ts b/src/app/student/run-info.ts index 5dd5e449ad6..1ff607b851b 100644 --- a/src/app/student/run-info.ts +++ b/src/app/student/run-info.ts @@ -8,4 +8,5 @@ export class RunInfo { error: string; name: string; wiseVersion: number; + isSurvey: boolean; } diff --git a/src/app/student/student.component.ts b/src/app/student/student.component.ts index e80097f191b..06ea93d924f 100644 --- a/src/app/student/student.component.ts +++ b/src/app/student/student.component.ts @@ -2,10 +2,10 @@ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; @Component({ - selector: 'app-student', - templateUrl: './student.component.html', - styleUrls: ['./student.component.scss'], - standalone: false + selector: 'app-student', + templateUrl: './student.component.html', + styleUrls: ['./student.component.scss'], + standalone: false }) export class StudentComponent implements OnInit { constructor(private router: Router) {} @@ -13,6 +13,10 @@ export class StudentComponent implements OnInit { ngOnInit() {} isShowingAngularJSApp() { - return this.router.url.includes('/student/unit') || this.router.url.includes('/preview/unit'); + return ( + this.router.url.includes('/student/unit') || + this.router.url.includes('/preview/unit') || + this.router.url.includes('/survey') + ); } } diff --git a/src/app/student/survey/survey-completed/survey-completed.component.html b/src/app/student/survey/survey-completed/survey-completed.component.html new file mode 100644 index 00000000000..e1ec8d73ac7 --- /dev/null +++ b/src/app/student/survey/survey-completed/survey-completed.component.html @@ -0,0 +1,22 @@ +
+ + + +

Your responses have been submitted.

+

Thank you!

+
+
+
diff --git a/src/app/student/survey/survey-completed/survey-completed.component.scss b/src/app/student/survey/survey-completed/survey-completed.component.scss new file mode 100644 index 00000000000..d9477abdc8b --- /dev/null +++ b/src/app/student/survey/survey-completed/survey-completed.component.scss @@ -0,0 +1,3 @@ +mat-card { + margin: 0% 30%; +} \ No newline at end of file diff --git a/src/app/student/survey/survey-completed/survey-completed.component.spec.ts b/src/app/student/survey/survey-completed/survey-completed.component.spec.ts new file mode 100644 index 00000000000..10ced8d5e5a --- /dev/null +++ b/src/app/student/survey/survey-completed/survey-completed.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SurveyCompletedComponent } from './survey-completed.component'; + +describe('SurveyCompletedComponent', () => { + let component: SurveyCompletedComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SurveyCompletedComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(SurveyCompletedComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/student/survey/survey-completed/survey-completed.component.ts b/src/app/student/survey/survey-completed/survey-completed.component.ts new file mode 100644 index 00000000000..73950b130fc --- /dev/null +++ b/src/app/student/survey/survey-completed/survey-completed.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; +import { MatCardModule } from '@angular/material/card'; + +@Component({ + selector: 'survey-completed', + imports: [MatCardModule], + templateUrl: './survey-completed.component.html', + styleUrl: './survey-completed.component.scss' +}) +export class SurveyCompletedComponent {} diff --git a/src/app/student/survey/survey-routing.module.ts b/src/app/student/survey/survey-routing.module.ts new file mode 100644 index 00000000000..bbb1ac8fec4 --- /dev/null +++ b/src/app/student/survey/survey-routing.module.ts @@ -0,0 +1,27 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { StudentComponent } from '../student.component'; +import { SurveyCompletedComponent } from './survey-completed/survey-completed.component'; +import { WorkgroupLimitReachedComponent } from './workgroup-limit-reached/workgroup-limit-reached.component'; + +const studentRoutes: Routes = [ + { + path: '', + component: StudentComponent, + children: [ + { path: '', redirectTo: '/', pathMatch: 'full' }, + { path: 'completed', component: SurveyCompletedComponent, pathMatch: 'full' }, + { + path: 'workgroupLimitReached', + component: WorkgroupLimitReachedComponent, + pathMatch: 'full' + } + ] + } +]; + +@NgModule({ + imports: [RouterModule.forChild(studentRoutes)], + exports: [RouterModule] +}) +export class SurveyRoutingModule {} diff --git a/src/app/student/survey/survey.module.ts b/src/app/student/survey/survey.module.ts new file mode 100644 index 00000000000..2c41a9c3914 --- /dev/null +++ b/src/app/student/survey/survey.module.ts @@ -0,0 +1,7 @@ +import { NgModule } from '@angular/core'; +import { SurveyRoutingModule } from './survey-routing.module'; + +@NgModule({ + imports: [SurveyRoutingModule] +}) +export class SurveyModule {} diff --git a/src/app/student/survey/workgroup-limit-reached/workgroup-limit-reached.component.html b/src/app/student/survey/workgroup-limit-reached/workgroup-limit-reached.component.html new file mode 100644 index 00000000000..43f66ae2fe8 --- /dev/null +++ b/src/app/student/survey/workgroup-limit-reached/workgroup-limit-reached.component.html @@ -0,0 +1,21 @@ +
+ + + +

Sorry, this unit has reached the maximum number of submissions.

+
+
+
diff --git a/src/app/student/survey/workgroup-limit-reached/workgroup-limit-reached.component.spec.ts b/src/app/student/survey/workgroup-limit-reached/workgroup-limit-reached.component.spec.ts new file mode 100644 index 00000000000..6a0a5b89252 --- /dev/null +++ b/src/app/student/survey/workgroup-limit-reached/workgroup-limit-reached.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { WorkgroupLimitReachedComponent } from './workgroup-limit-reached.component'; + +describe('WorkgroupLimitReachedComponent', () => { + let component: WorkgroupLimitReachedComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [WorkgroupLimitReachedComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(WorkgroupLimitReachedComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/student/survey/workgroup-limit-reached/workgroup-limit-reached.component.ts b/src/app/student/survey/workgroup-limit-reached/workgroup-limit-reached.component.ts new file mode 100644 index 00000000000..3ad55ad5d7d --- /dev/null +++ b/src/app/student/survey/workgroup-limit-reached/workgroup-limit-reached.component.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; +import { MatCardModule } from '@angular/material/card'; + +@Component({ + selector: 'workgroup-limit-reached', + imports: [MatCardModule], + templateUrl: './workgroup-limit-reached.component.html' +}) +export class WorkgroupLimitReachedComponent {} diff --git a/src/app/teacher/create-run-dialog/create-run-dialog.component.html b/src/app/teacher/create-run-dialog/create-run-dialog.component.html index 85f7783d72f..3394718c77a 100644 --- a/src/app/teacher/create-run-dialog/create-run-dialog.component.html +++ b/src/app/teacher/create-run-dialog/create-run-dialog.component.html @@ -1,123 +1,173 @@

Use with Class

-
- -

- {{ project.metadata.title }} (Unit ID: {{ project.id }}) -

-

1. Choose Periods

-

- - - {{ control.controls.name.value }} - - -

- - Add your own periods - - -

- For "Period 9", just enter the number 9. Separate periods with commas (e.g. "Section 1, - Section 2"). Manually named periods should be no more than 16 characters long. -

- -

2. Choose Students Per Team

- - Only 1 student - 1-3 students - - -

3. Set Schedule

-
-
- - Start date - - - - Start date is required - -
-
- - End date - - - - -
-
- - Lock After End Date   +@if (!isCreated) { + + +

+ {{ project.metadata.title }} + (Unit ID: {{ project.id }}) +

+

1. Choose Run Type

+
+ + Default + Survey + + help
-
-

- Note: These dates can be changed at any time from your Class Schedule. Just select "Edit - Settings" from the unit's dropdown menu. -

- - - - - - - + + @if (isDefaultRun()) { +

2. Choose Periods

+

+ @for (control of selectedPeriodsControl.controls; track $index) { + + + {{ control.controls.name.value }} + + + } +

+ + Add your own periods + + +

+ For "Period 9", just enter the number 9. Separate periods with commas (e.g. "Section 1, + Section 2"). Manually named periods should be no more than 16 characters long. +

+ +

3. Choose Students Per Team

+ + Only 1 student + 1-3 students + + +

4. Set Schedule

+ } @else { +

2. Set Schedule

+ } +
+
+ + Start date + + + + Start date is required + +
+
+ + End date + + + + +
+ @if (isDefaultRun()) { +
+ + Lock After End Date  + help +
+ } +
+

+ Note: These dates can be changed at any time from your Class Schedule. Just select "Edit + Settings" from the unit's dropdown menu. +

+ + + + + + +} @else {

Success! This unit has been added to your Class Schedule.

{{ run.name }}

-

Access Code: {{ run.runCode }}

-

- Important: Every classroom unit has a unique Access Code. Students use this code to register - for a unit. Give the code to your students when they first sign up for a WISE account. If - they already have WISE accounts, have them log in and then select "Add Unit" from the - student home page. -

-

- You can always find the Access Code for each classroom unit in your Class Schedule. -

+ @if (isDefaultRun()) { +

Access Code: {{ run.runCode }}

+

+ Important: Every classroom unit has a unique Access Code. Students use this code to + register for a unit. Give the code to your students when they first sign up for a WISE + account. If they already have WISE accounts, have them log in and then select "Add Unit" + from the student home page. +

+

+ You can always find the Access Code for each classroom unit in your Class Schedule. +

+ } @else { +

+ Access Link: + +

+

+ Important: Every survey unit has a unique Access Link. Participants can use this link to + complete the unit without a WISE account. +

+

You can always find the Access Link for each survey unit in your Class Schedule.

+ }
- + @if (isGoogleUser() && isGoogleClassroomEnabled()) { + + } -
+} diff --git a/src/app/teacher/create-run-dialog/create-run-dialog.component.scss b/src/app/teacher/create-run-dialog/create-run-dialog.component.scss index 3140da7417d..6d21ba01128 100644 --- a/src/app/teacher/create-run-dialog/create-run-dialog.component.scss +++ b/src/app/teacher/create-run-dialog/create-run-dialog.component.scss @@ -5,3 +5,7 @@ .period-input { width: 100%; } + +.access-link { + text-transform: lowercase; +} \ No newline at end of file diff --git a/src/app/teacher/create-run-dialog/create-run-dialog.component.spec.ts b/src/app/teacher/create-run-dialog/create-run-dialog.component.spec.ts index 1a04d0b61d9..956397f296e 100644 --- a/src/app/teacher/create-run-dialog/create-run-dialog.component.spec.ts +++ b/src/app/teacher/create-run-dialog/create-run-dialog.component.spec.ts @@ -1,19 +1,19 @@ +import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; +import { By } from '@angular/platform-browser'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { TeacherService } from '../teacher.service'; +import { ConfigService } from '../../services/config.service'; +import { Course } from '../../domain/course'; import { CreateRunDialogComponent } from './create-run-dialog.component'; import { MatDialogRef, MatDialog } from '@angular/material/dialog'; import { MAT_DIALOG_DATA } from '@angular/material/dialog'; import { Observable } from 'rxjs'; import { of } from 'rxjs'; import { Project } from '../../domain/project'; +import { Router } from '@angular/router'; import { Run } from '../../domain/run'; -import { By } from '@angular/platform-browser'; -import { Course } from '../../domain/course'; -import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; +import { TeacherService } from '../teacher.service'; import { User } from '../../domain/user'; import { UserService } from '../../services/user.service'; -import { ConfigService } from '../../services/config.service'; -import { Router } from '@angular/router'; export class MockTeacherService { createRun() { @@ -135,10 +135,29 @@ describe('CreateRunDialogComponent', () => { component.periodsGroup.controls[0].get('checkbox').setValue(true); component.periodsGroup.controls[2].get('checkbox').setValue(true); component.periodsGroup.controls[4].get('checkbox').setValue(true); - component.customPeriods.setValue('hello'); + component['customPeriods'].setValue('hello'); expect(component.getPeriodsString()).toEqual('1,3,5,hello'); }); + it('should not show period or max workgroup size options if survey is checked', () => { + let h3ElementsText = Array.from(fixture.nativeElement.querySelectorAll('h3')).map( + (element: HTMLElement) => element.innerText + ); + expect(h3ElementsText.length).toEqual(4); + expect(h3ElementsText.includes('2. Choose Periods')).toBeTrue(); + expect(h3ElementsText.includes('3. Choose Students Per Team')).toBeTrue(); + + component.form.controls['runType'].setValue('survey'); + fixture.detectChanges(); + + h3ElementsText = Array.from(fixture.nativeElement.querySelectorAll('h3')).map( + (element: HTMLElement) => element.innerText + ); + expect(h3ElementsText.length).toEqual(2); + expect(h3ElementsText.includes('2. Choose Periods')).toBeFalse(); + expect(h3ElementsText.includes('3. Choose Students Per Team')).toBeFalse(); + }); + it('should disable submit button and invalidate form on initial state (when no period is selected)', () => { const submitButton = getSubmitButton(); expect(component.form.valid).toBeFalsy(); @@ -150,7 +169,7 @@ describe('CreateRunDialogComponent', () => { fixture.detectChanges(); expect(component.form.valid).toBeTruthy(); component.periodsGroup.controls[0].get('checkbox').setValue(false); - component.customPeriods.setValue('Section A, Section B'); + component['customPeriods'].setValue('Section A, Section B'); fixture.detectChanges(); expect(component.form.valid).toBeTruthy(); }); @@ -185,6 +204,7 @@ describe('CreateRunDialogComponent', () => { expect(teacherService.createRun).toHaveBeenCalledWith( 1, '1,', + false, '3', jasmine.any(Number), jasmine.any(Number), @@ -205,6 +225,7 @@ describe('CreateRunDialogComponent', () => { expect(teacherService.createRun).toHaveBeenCalledWith( 1, '1,', + false, '3', jasmine.any(Number), jasmine.any(Number), diff --git a/src/app/teacher/create-run-dialog/create-run-dialog.component.ts b/src/app/teacher/create-run-dialog/create-run-dialog.component.ts index beaa061d1e1..ae689a58345 100644 --- a/src/app/teacher/create-run-dialog/create-run-dialog.component.ts +++ b/src/app/teacher/create-run-dialog/create-run-dialog.component.ts @@ -1,5 +1,8 @@ -import { Component, Inject } from '@angular/core'; +import { ClipboardModule } from '@angular/cdk/clipboard'; import { CommonModule } from '@angular/common'; +import { Component, Inject } from '@angular/core'; +import { ConfigService } from '../../services/config.service'; +import { finalize } from 'rxjs/operators'; import { FormsModule, ReactiveFormsModule, @@ -9,85 +12,87 @@ import { FormGroup, Validators } from '@angular/forms'; +import { ListClassroomCoursesDialogComponent } from '../list-classroom-courses-dialog/list-classroom-courses-dialog.component'; import { MatButtonModule } from '@angular/material/button'; +import { MatCardModule } from '@angular/material/card'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA, MatDialog } from '@angular/material/dialog'; -import { MatInputModule } from '@angular/material/input'; -import { MatCheckboxModule } from '@angular/material/checkbox'; -import { MatDatepickerModule } from '@angular/material/datepicker'; -import { provideNativeDateAdapter } from '@angular/material/core'; +import { MatDividerModule } from '@angular/material/divider'; +import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from '@angular/material/input'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { MatRadioModule } from '@angular/material/radio'; +import { MatSnackBar } from '@angular/material/snack-bar'; import { MatTooltipModule } from '@angular/material/tooltip'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatCardModule } from '@angular/material/card'; -import { Router } from '@angular/router'; -import { finalize } from 'rxjs/operators'; import { Project } from '../../domain/project'; +import { provideNativeDateAdapter } from '@angular/material/core'; +import { Router } from '@angular/router'; +import { TeacherRun } from '../teacher-run'; import { TeacherService } from '../teacher.service'; import { UserService } from '../../services/user.service'; -import { ConfigService } from '../../services/config.service'; -import { ListClassroomCoursesDialogComponent } from '../list-classroom-courses-dialog/list-classroom-courses-dialog.component'; -import { TeacherRun } from '../teacher-run'; -import { MatDividerModule } from '@angular/material/divider'; -import { MatRadioModule } from '@angular/material/radio'; -import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { AccessLinkService } from '../../services/accessLinkService'; @Component({ imports: [ + ClipboardModule, CommonModule, FormsModule, - ReactiveFormsModule, MatButtonModule, - MatDialogModule, - MatDividerModule, - MatInputModule, + MatCardModule, MatCheckboxModule, MatDatepickerModule, + MatDialogModule, + MatDividerModule, + MatFormFieldModule, MatIconModule, + MatInputModule, + MatProgressBarModule, MatRadioModule, MatTooltipModule, - MatFormFieldModule, - MatProgressBarModule, - MatCardModule + ReactiveFormsModule ], - providers: [provideNativeDateAdapter()], + providers: [AccessLinkService, provideNativeDateAdapter()], selector: 'create-run-dialog', styleUrl: './create-run-dialog.component.scss', templateUrl: './create-run-dialog.component.html' }) export class CreateRunDialogComponent { + protected accessLinks: string[] = []; + protected customPeriods: FormControl; + private endDateControl: FormControl; form: FormGroup; - project: Project; + protected isCreated: boolean = false; + protected isCreating: boolean = false; + protected maxStartDate: Date; + protected minEndDate: Date; + private periodOptions: string[] = []; periodsGroup: FormArray; - customPeriods: FormControl; - maxStudentsPerTeam: number; - maxStartDate: Date; - minEndDate: Date; - endDateControl: FormControl; - periodOptions: string[] = []; - isCreating: boolean = false; - isCreated: boolean = false; + project: Project; run: TeacherRun = null; constructor( + private accessLinkService: AccessLinkService, private configService: ConfigService, @Inject(MAT_DIALOG_DATA) public data: any, public dialog: MatDialog, public dialogRef: MatDialogRef, private fb: FormBuilder, private router: Router, + private snackBar: MatSnackBar, private teacherService: TeacherService, private userService: UserService ) { this.project = data.project; - this.maxStudentsPerTeam = 3; } - ngOnInit() { + ngOnInit(): void { this.setPeriodOptions(); let hiddenControl = new FormControl('', Validators.required); this.periodsGroup = new FormArray( @@ -114,6 +119,7 @@ export class CreateRunDialogComponent { selectedPeriods: this.periodsGroup, customPeriods: this.customPeriods, periods: hiddenControl, + runType: new FormControl('default', Validators.required), maxStudentsPerTeam: new FormControl('3', Validators.required), startDate: new FormControl(new Date(), Validators.required), endDate: this.endDateControl, @@ -122,15 +128,15 @@ export class CreateRunDialogComponent { this.setDateRange(); } - isGoogleUser() { + protected isGoogleUser(): boolean { return this.userService.isGoogleUser(); } - isGoogleClassroomEnabled() { + protected isGoogleClassroomEnabled(): boolean { return this.configService.isGoogleClassroomEnabled(); } - setPeriodOptions() { + private setPeriodOptions(): void { for (let i = 1; i < 9; i++) { this.periodOptions.push(i.toString()); } @@ -140,27 +146,33 @@ export class CreateRunDialogComponent { return this.form.get('selectedPeriods'); } - mapPeriods(items: any[]): string[] { + private mapPeriods(items: any[]): string[] { const selectedPeriods = items.filter((item) => item.checkbox).map((item) => item.name); return selectedPeriods.length ? selectedPeriods : []; } - create() { + create(): void { this.isCreating = true; - const combinedPeriods = this.getPeriodsString(); - const startDate = this.form.controls['startDate'].value.getTime(); - let endDateValue = this.form.controls['endDate'].value; - let endDate = null; + const isSurvey: boolean = this.getFormControlValue('runType') === 'survey'; + const combinedPeriods = isSurvey + ? this.mapPeriods(this.periodsGroup.value).toString() + : this.getPeriodsString(); + const startDate: number = this.getFormControlValue('startDate').getTime(); + let endDateValue: Date = this.getFormControlValue('endDate'); + let endDate: number = null; if (endDateValue) { endDateValue.setHours(23, 59, 59); endDate = endDateValue.getTime(); } - const isLockedAfterEndDate = this.form.controls['isLockedAfterEndDate'].value; - const maxStudentsPerTeam = this.form.controls['maxStudentsPerTeam'].value; + const isLockedAfterEndDate: boolean = this.getFormControlValue('isLockedAfterEndDate'); + const maxStudentsPerTeam: number = isSurvey + ? 1 + : this.getFormControlValue('maxStudentsPerTeam'); this.teacherService .createRun( this.project.id, combinedPeriods, + isSurvey, maxStudentsPerTeam, startDate, endDate, @@ -173,6 +185,12 @@ export class CreateRunDialogComponent { ) .subscribe((newRun: TeacherRun) => { this.run = new TeacherRun(newRun); + if (this.run.isSurveyRun()) { + this.accessLinks = this.accessLinkService.getAccessLinks( + this.run.runCode, + this.run.periods + ); + } this.dialogRef.afterClosed().subscribe(() => { this.router.navigate(['/teacher/home/schedule'], { queryParams: { newRunId: newRun.id } @@ -195,16 +213,16 @@ export class CreateRunDialogComponent { } } - setDateRange() { - this.minEndDate = this.form.controls['startDate'].value; - this.maxStartDate = this.form.controls['endDate'].value; + protected setDateRange(): void { + this.minEndDate = this.getFormControlValue('startDate'); + this.maxStartDate = this.getFormControlValue('endDate'); } - closeAll() { + protected closeAll(): void { this.dialog.closeAll(); } - checkClassroomAuthorization() { + protected checkClassroomAuthorization(): void { this.teacherService .getClassroomAuthorizationUrl(this.userService.getUser().getValue().username) .subscribe(({ authorizationUrl }) => { @@ -222,7 +240,7 @@ export class CreateRunDialogComponent { }); } - getClassroomCourses() { + private getClassroomCourses(): void { this.teacherService .getClassroomCourses(this.userService.getUser().getValue().username) .subscribe((courses) => { @@ -233,7 +251,7 @@ export class CreateRunDialogComponent { }); } - updateLockedAfterEndDateCheckbox() { + updateLockedAfterEndDateCheckbox(): void { if (this.endDateControl.value == null) { this.form.controls['isLockedAfterEndDate'].setValue(false); this.form.controls['isLockedAfterEndDate'].disable(); @@ -241,4 +259,29 @@ export class CreateRunDialogComponent { this.form.controls['isLockedAfterEndDate'].enable(); } } + + protected isDefaultRun(): boolean { + return this.getFormControlValue('runType') === 'default'; + } + + private getFormControlValue(control: string): any { + return this.form.controls[control].value; + } + + protected copyMsg(): void { + this.snackBar.open($localize`Copied to clipboard.`); + } + + protected getPeriodFromAccessLink(link: string): string { + return this.accessLinkService.getPeriodFromAccessLink(link); + } + + protected setAsSurveyUnit(): void { + this.customPeriods.setValue(''); + this.periodsGroup.controls.forEach((control, index) => { + index === 0 + ? control.get('checkbox').setValue(true) + : control.get('checkbox').setValue(false); + }); + } } diff --git a/src/app/teacher/run-settings-dialog/run-settings-dialog.component.html b/src/app/teacher/run-settings-dialog/run-settings-dialog.component.html index 0c6de51e89c..272eba9b018 100644 --- a/src/app/teacher/run-settings-dialog/run-settings-dialog.component.html +++ b/src/app/teacher/run-settings-dialog/run-settings-dialog.component.html @@ -3,65 +3,69 @@

Edit Settings

{{ run.name }} (Run ID: {{ run.id }})

-

Class Periods

-
-
- {{ period }} - + @if (isDefaultRun) { +

Class Periods

+
+
+ {{ period }} + +
-
-
- - Add new period - - - {{ addPeriodMessage }} - -

- For "Period 9", just enter the number 9. -

-
- -

Students Per Team

- - - Only 1 student - - - 1-3 students - - - +
+ + Add new period + + + {{ addPeriodMessage }} + +

+ For "Period 9", just enter the number 9. +

+
+ +

Students Per Team

+ + + Only 1 student + + + 1-3 students + + + + }

Schedule  - - (Last student login: {{ run.lastRun | date: 'short' }}) - + @if (run.lastRun) { + + (Last student login: {{ run.lastRun | date: 'short' }}) + + }

@@ -95,23 +99,25 @@

{{ endDateMessage }}

-
- - Lock After End Date   - help -
+ @if (isDefaultRun) { +
+ + Lock After End Date   + help +
+ }
diff --git a/src/app/teacher/run-settings-dialog/run-settings-dialog.component.spec.ts b/src/app/teacher/run-settings-dialog/run-settings-dialog.component.spec.ts index 9bc4973b17e..abfdcee4150 100644 --- a/src/app/teacher/run-settings-dialog/run-settings-dialog.component.spec.ts +++ b/src/app/teacher/run-settings-dialog/run-settings-dialog.component.spec.ts @@ -53,10 +53,9 @@ export class MockTeacherService { } } +let component: RunSettingsDialogComponent; +let fixture: ComponentFixture; describe('RunSettingsDialogComponent', () => { - let component: RunSettingsDialogComponent; - let fixture: ComponentFixture; - const getStartDateInput = () => { return fixture.debugElement.nativeElement.querySelectorAll('input')[1]; }; @@ -199,4 +198,20 @@ describe('RunSettingsDialogComponent', () => { const message = component.translateMessageCode('periodNameAlreadyExists'); expect(message).toEqual('There is already a period with that name.'); }); + + surveyRun(); }); + +function surveyRun() { + describe('Survey Run', () => { + beforeEach(() => { + component['isDefaultRun'] = false; + fixture.detectChanges(); + }); + + it('should hide Student Per Team section', () => { + const radioGroup = fixture.debugElement.nativeElement.querySelector('mat-radio-group'); + expect(radioGroup).toBeNull(); + }); + }); +} diff --git a/src/app/teacher/run-settings-dialog/run-settings-dialog.component.ts b/src/app/teacher/run-settings-dialog/run-settings-dialog.component.ts index 719e371c141..306f6d4926e 100644 --- a/src/app/teacher/run-settings-dialog/run-settings-dialog.component.ts +++ b/src/app/teacher/run-settings-dialog/run-settings-dialog.component.ts @@ -7,10 +7,10 @@ import { formatDate } from '@angular/common'; import { TeacherRun } from '../teacher-run'; @Component({ - selector: 'app-run-settings-dialog', - templateUrl: './run-settings-dialog.component.html', - styleUrls: ['./run-settings-dialog.component.scss'], - standalone: false + selector: 'app-run-settings-dialog', + templateUrl: './run-settings-dialog.component.html', + styleUrls: ['./run-settings-dialog.component.scss'], + standalone: false }) export class RunSettingsDialogComponent implements OnInit { run: TeacherRun; @@ -28,6 +28,7 @@ export class RunSettingsDialogComponent implements OnInit { startDateMessage: string = ''; endDateMessage: string = ''; isLockedAfterEndDateMessage: string = ''; + protected isDefaultRun: boolean = true; maxStartDate: Date; minEndDate: Date; targetEndDate: Date; @@ -53,6 +54,7 @@ export class RunSettingsDialogComponent implements OnInit { this.isLockedAfterEndDateCheckboxEnabled = true; } this.initializeMessageCodeToMessage(); + this.isDefaultRun = !this.run.isSurveyRun(); } initializeMessageCodeToMessage() { diff --git a/src/app/teacher/share-run-code-dialog/share-run-code-dialog.component.html b/src/app/teacher/share-run-code-dialog/share-run-code-dialog.component.html index 98f38db818d..57087867790 100644 --- a/src/app/teacher/share-run-code-dialog/share-run-code-dialog.component.html +++ b/src/app/teacher/share-run-code-dialog/share-run-code-dialog.component.html @@ -1,60 +1,87 @@ -

Share with Students

+@if (run.isSurveyRun()) { +

Share with Participants

+} @else { +

Share with Students

+}

{{ run.project.name }} (Run ID: {{ run.id }})

-

Copy this link to share with your students:

- -

- Students with WISE accounts can also select Add Unit+ and type the Access - Code: -

- - -

Add as an assignment in Google Classroom:

+ @if (run.isSurveyRun()) { +

+ This is a survey unit. Participants complete survey units anonymously and do not need a WISE + account. +

+

Copy this link to share with participants:

+ + } @else { +

Copy this link to share with your students:

-
+

+ Students with WISE accounts can also select Add Unit+ and type the Access + Code: +

+ + +

Add as an assignment in Google Classroom:

+ +
+ }
diff --git a/src/app/teacher/share-run-code-dialog/share-run-code-dialog.component.spec.ts b/src/app/teacher/share-run-code-dialog/share-run-code-dialog.component.spec.ts index fcae1fd4a53..2b0a592941b 100644 --- a/src/app/teacher/share-run-code-dialog/share-run-code-dialog.component.spec.ts +++ b/src/app/teacher/share-run-code-dialog/share-run-code-dialog.component.spec.ts @@ -3,16 +3,21 @@ import { ShareRunCodeDialogComponent } from './share-run-code-dialog.component'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { MatSnackBarModule } from '@angular/material/snack-bar'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ConfigService } from '../../services/config.service'; import { TeacherService } from '../teacher.service'; -import { UserService } from '../../services/user.service'; import { TeacherRun } from '../teacher-run'; import { Project } from '../../domain/project'; +import { AccessLinkService } from '../../services/accessLinkService'; +import { MatIconModule } from '@angular/material/icon'; +import { ClipboardModule } from '@angular/cdk/clipboard'; +import { MockProviders } from 'ng-mocks'; +import { CommonModule } from '@angular/common'; +import { UserService } from '../../services/user.service'; const runObj = new TeacherRun(); runObj.id = 1; runObj.runCode = 'Dog123'; +runObj.isSurveyRun = () => false; const project = new Project(); project.id = 1; project.name = 'Photosynthesis'; @@ -32,47 +37,73 @@ export class MockConfigService { } } -export class MockTeacherService {} - -export class MockUserService { - isGoogleUser() { - return true; - } -} - +let component: ShareRunCodeDialogComponent; +let fixture: ComponentFixture; describe('ShareRunCodeDialogComponent', () => { - let component: ShareRunCodeDialogComponent; - let fixture: ComponentFixture; - beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [ShareRunCodeDialogComponent], - imports: [BrowserAnimationsModule, MatDialogModule, MatSnackBarModule], + imports: [ + BrowserAnimationsModule, + CommonModule, + ClipboardModule, + MatDialogModule, + MatIconModule, + MatSnackBarModule + ], providers: [ + MockProviders(AccessLinkService, MatDialogRef, TeacherService, UserService), { provide: ConfigService, useClass: MockConfigService }, - { provide: TeacherService, useClass: MockTeacherService }, - { provide: UserService, useClass: MockUserService }, - { provide: MatDialogRef, useValue: {} }, { provide: MAT_DIALOG_DATA, useValue: runObj } - ], - schemas: [NO_ERRORS_SCHEMA] + ] }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(ShareRunCodeDialogComponent); component = fixture.componentInstance; + component.run.isSurveyRun = () => false; fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('should show run info', () => { + expect(fixture.debugElement.nativeElement.textContent).toContain('Photosynthesis (Run ID: 1)'); }); - it('should show run info and sharing url', () => { - const compiled = fixture.debugElement.nativeElement; - expect(compiled.textContent).toContain('Photosynthesis (Run ID: 1)'); - const url = `http://localhost:8080/login?accessCode=${component.run.runCode}`; - expect(compiled.textContent).toContain(url); - }); + defaultRun(); + surveyRun(); }); + +function defaultRun() { + describe('when run is a default run', () => { + beforeEach(() => { + component.run.isSurveyRun = () => false; + fixture.detectChanges(); + }); + it('should show access links to share with students', () => { + const textContent = fixture.debugElement.nativeElement.textContent; + expect(textContent).toContain('Copy this link to share with your students:'); + expect(textContent).toContain( + `http://localhost:8080/login?accessCode=${component.run.runCode}` + ); + }); + }); +} + +function surveyRun() { + describe('when run is a survey run', () => { + beforeEach(() => { + component.run.isSurveyRun = () => true; + spyOn(TestBed.inject(AccessLinkService), 'getAccessLinks').and.returnValue([ + 'http://localhost:8080/run-survey/Dog123-1' + ]); + component.ngOnInit(); + fixture.detectChanges(); + }); + it('should show access links to share with participants', () => { + const textContent = fixture.debugElement.nativeElement.textContent; + expect(textContent).toContain('Copy this link to share with participants:'); + expect(textContent).toContain(`http://localhost:8080/run-survey/Dog123-1`); + }); + }); +} diff --git a/src/app/teacher/share-run-code-dialog/share-run-code-dialog.component.ts b/src/app/teacher/share-run-code-dialog/share-run-code-dialog.component.ts index 927ef5ac4b8..775dbc21b3f 100644 --- a/src/app/teacher/share-run-code-dialog/share-run-code-dialog.component.ts +++ b/src/app/teacher/share-run-code-dialog/share-run-code-dialog.component.ts @@ -6,19 +6,22 @@ import { UserService } from '../../services/user.service'; import { TeacherRun } from '../teacher-run'; import { TeacherService } from '../teacher.service'; import { ListClassroomCoursesDialogComponent } from '../list-classroom-courses-dialog/list-classroom-courses-dialog.component'; +import { AccessLinkService } from '../../services/accessLinkService'; @Component({ - selector: 'app-share-run-code-dialog', - templateUrl: './share-run-code-dialog.component.html', - styleUrls: ['./share-run-code-dialog.component.scss'], - standalone: false + selector: 'app-share-run-code-dialog', + templateUrl: './share-run-code-dialog.component.html', + styleUrls: ['./share-run-code-dialog.component.scss'], + standalone: false }) export class ShareRunCodeDialogComponent { - code: string; - link: string; + protected accessLinks: string[] = []; + protected code: string; + protected link: string; constructor( @Inject(MAT_DIALOG_DATA) public run: TeacherRun, + private accessLinkService: AccessLinkService, private dialog: MatDialog, private snackBar: MatSnackBar, private teacherService: TeacherService, @@ -26,25 +29,28 @@ export class ShareRunCodeDialogComponent { private configService: ConfigService ) {} - ngOnInit() { + ngOnInit(): void { this.code = this.run.runCode; const host = this.configService.getWISEHostname() + this.configService.getContextPath(); this.link = `${host}/login?accessCode=${this.code}`; + if (this.run.isSurveyRun()) { + this.accessLinks = this.accessLinkService.getAccessLinks(this.run.runCode, this.run.periods); + } } - copyMsg() { + protected copyMsg(): void { this.snackBar.open($localize`Copied to clipboard.`); } - isGoogleUser() { + protected isGoogleUser(): boolean { return this.userService.isGoogleUser(); } - isGoogleClassroomEnabled() { + protected isGoogleClassroomEnabled(): boolean { return this.configService.isGoogleClassroomEnabled(); } - checkClassroomAuthorization() { + protected checkClassroomAuthorization(): void { this.teacherService .getClassroomAuthorizationUrl(this.userService.getUser().getValue().username) .subscribe(({ authorizationUrl }) => { @@ -62,7 +68,7 @@ export class ShareRunCodeDialogComponent { }); } - getClassroomCourses() { + private getClassroomCourses(): void { this.teacherService .getClassroomCourses(this.userService.getUser().getValue().username) .subscribe((courses) => { @@ -73,4 +79,8 @@ export class ShareRunCodeDialogComponent { }); }); } + + protected getPeriodFromAccessLink(link: string): string { + return this.accessLinkService.getPeriodFromAccessLink(link); + } } 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 55ef1c3c828..567eccebc72 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 @@ -36,13 +36,19 @@
- Access Code: {{ run.runCode }} (Share with students) + @if (run.isSurveyRun()) { + Survey Unit (Share with participants) + } @else { + Access Code: {{ run.runCode }} (Share with students) + }
Shared by {{ run.owner.firstName }} {{ run.owner.lastName }} diff --git a/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.scss b/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.scss index bc0dc87e74f..9b1d5b138a2 100644 --- a/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.scss +++ b/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.scss @@ -28,3 +28,12 @@ margin: -8px; display: block; } + +.access-link { + text-transform: lowercase; +} + +.toggle-show:hover { + cursor: pointer; + text-decoration: underline; +} \ No newline at end of file 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 276f1bd15b5..4b65661c4e8 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 @@ -1,27 +1,27 @@ +import { ArchiveProjectResponse } from '../../domain/archiveProjectResponse'; +import { ArchiveProjectService } from '../../services/archive-project.service'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { TeacherRunListItemComponent } from './teacher-run-list-item.component'; -import { TeacherService } from '../teacher.service'; -import { TeacherRun } from '../teacher-run'; import { ConfigService } from '../../services/config.service'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { HttpClient, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; +import { MatCardModule } from '@angular/material/card'; import { MatDialogModule } from '@angular/material/dialog'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { RouterTestingModule } from '@angular/router/testing'; -import { TeacherRunListItemHarness } from './teacher-run-list-item.harness'; -import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { MatIconModule } from '@angular/material/icon'; import { MatMenuModule } from '@angular/material/menu'; -import { RunMenuComponent } from '../run-menu/run-menu.component'; -import { ArchiveProjectService } from '../../services/archive-project.service'; -import { provideHttpClientTesting } from '@angular/common/http/testing'; -import { UserService } from '../../services/user.service'; import { MatSnackBarModule } from '@angular/material/snack-bar'; -import { MatCardModule } from '@angular/material/card'; -import { MatIconModule } from '@angular/material/icon'; -import { User } from '../../domain/user'; -import { HttpClient, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; import { of } from 'rxjs'; -import { ArchiveProjectResponse } from '../../domain/archiveProjectResponse'; import { ProjectTagService } from '../../../assets/wise5/services/projectTagService'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { RunMenuComponent } from '../run-menu/run-menu.component'; +import { TeacherRun } from '../teacher-run'; +import { TeacherRunListItemComponent } from './teacher-run-list-item.component'; +import { TeacherRunListItemHarness } from './teacher-run-list-item.harness'; +import { TeacherService } from '../teacher.service'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { User } from '../../domain/user'; +import { UserService } from '../../services/user.service'; export class MockTeacherService {} @@ -52,16 +52,18 @@ let userService: UserService; describe('TeacherRunListItemComponent', () => { beforeEach(async () => { TestBed.configureTestingModule({ - declarations: [RunMenuComponent, TeacherRunListItemComponent], - schemas: [NO_ERRORS_SCHEMA], - imports: [BrowserAnimationsModule, + declarations: [RunMenuComponent, TeacherRunListItemComponent], + schemas: [NO_ERRORS_SCHEMA], + imports: [ + BrowserAnimationsModule, MatCardModule, MatDialogModule, MatIconModule, MatMenuModule, MatSnackBarModule, - RouterTestingModule], - providers: [ + RouterTestingModule + ], + providers: [ ArchiveProjectService, { provide: ConfigService, useClass: MockConfigService }, ProjectTagService, @@ -69,8 +71,8 @@ describe('TeacherRunListItemComponent', () => { UserService, provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting() - ] -}); + ] + }); http = TestBed.inject(HttpClient); userService = TestBed.inject(UserService); spyOn(userService, 'getUserId').and.returnValue(userId); 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 48bda33ff6e..9efa3ae6ae7 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 @@ -1,22 +1,22 @@ import { Component, OnInit, Input, ElementRef, Output, EventEmitter } from '@angular/core'; -import { DomSanitizer } from '@angular/platform-browser'; -import { SafeStyle } from '@angular/platform-browser'; -import { TeacherRun } from '../teacher-run'; import { ConfigService } from '../../services/config.service'; +import { DomSanitizer } from '@angular/platform-browser'; import { flash } from '../../animations'; -import { Router } from '@angular/router'; import { MatDialog } from '@angular/material/dialog'; +import { ProjectTagService } from '../../../assets/wise5/services/projectTagService'; +import { Router } from '@angular/router'; +import { SafeStyle } from '@angular/platform-browser'; import { ShareRunCodeDialogComponent } from '../share-run-code-dialog/share-run-code-dialog.component'; import { Subscription } from 'rxjs'; -import { ProjectTagService } from '../../../assets/wise5/services/projectTagService'; import { Tag } from '../../domain/tag'; +import { TeacherRun } from '../teacher-run'; @Component({ - animations: [flash], - selector: 'app-teacher-run-list-item', - styleUrl: './teacher-run-list-item.component.scss', - templateUrl: './teacher-run-list-item.component.html', - standalone: false + animations: [flash], + selector: 'app-teacher-run-list-item', + standalone: false, + styleUrl: './teacher-run-list-item.component.scss', + templateUrl: './teacher-run-list-item.component.html' }) export class TeacherRunListItemComponent implements OnInit { protected animateDelay: string = '0s'; @@ -30,12 +30,12 @@ export class TeacherRunListItemComponent implements OnInit { protected thumbStyle: SafeStyle; constructor( - private sanitizer: DomSanitizer, private configService: ConfigService, - private router: Router, - private elRef: ElementRef, private dialog: MatDialog, - private projectTagService: ProjectTagService + private elRef: ElementRef, + private projectTagService: ProjectTagService, + private router: Router, + private sanitizer: DomSanitizer ) {} ngOnInit(): void { @@ -91,7 +91,7 @@ export class TeacherRunListItemComponent implements OnInit { this.subscriptions.unsubscribe(); } - private getThumbStyle() { + private getThumbStyle(): SafeStyle { const DEFAULT_THUMB = 'assets/img/default-picture.svg'; const STYLE = `url(${this.run.project.projectThumb}), url(${DEFAULT_THUMB})`; return this.sanitizer.bypassSecurityTrustStyle(STYLE); @@ -132,7 +132,7 @@ export class TeacherRunListItemComponent implements OnInit { return run.isCompleted(this.configService.getCurrentServerTime()); } - shareCode(event: Event): void { + protected shareCode(event: Event): void { event.preventDefault(); this.dialog.open(ShareRunCodeDialogComponent, { data: this.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 ff5bccb258b..6ce576f700b 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 @@ -33,6 +33,8 @@ import { ArchiveProjectResponse } from '../../domain/archiveProjectResponse'; import { Tag } from '../../domain/tag'; import { provideRouter } from '@angular/router'; import { ProjectTagService } from '../../../assets/wise5/services/projectTagService'; +import { MockProvider } from 'ng-mocks'; +import { AccessLinkService } from '../../services/accessLinkService'; class TeacherScheduleStubComponent {} @@ -103,6 +105,7 @@ describe('TeacherRunListComponent', () => { SelectRunsControlsModule ], providers: [ + MockProvider(AccessLinkService), ArchiveProjectService, ConfigService, ProjectTagService, diff --git a/src/app/teacher/teacher.module.ts b/src/app/teacher/teacher.module.ts index 09243099843..3bcfff3d4b9 100644 --- a/src/app/teacher/teacher.module.ts +++ b/src/app/teacher/teacher.module.ts @@ -48,6 +48,7 @@ import { SelectTagsComponent } from './select-tags/select-tags.component'; import { UnitTagsComponent } from './unit-tags/unit-tags.component'; import { ColorService } from '../../assets/wise5/services/colorService'; import { NgSelectModule } from '@ng-select/ng-select'; +import { AccessLinkService } from '../services/accessLinkService'; const materialModules = [ MatAutocompleteModule, @@ -103,7 +104,7 @@ const materialModules = [ TeacherRunListComponent, TeacherRunListItemComponent ], - providers: [AuthGuard, ColorService, ProjectTagService], + providers: [AccessLinkService, AuthGuard, ColorService, ProjectTagService], exports: [TeacherComponent, UnitTagsComponent, materialModules] }) export class TeacherModule {} diff --git a/src/app/teacher/teacher.service.ts b/src/app/teacher/teacher.service.ts index a691bad2e3d..68ee76b6284 100644 --- a/src/app/teacher/teacher.service.ts +++ b/src/app/teacher/teacher.service.ts @@ -1,12 +1,12 @@ -import { Injectable } from '@angular/core'; -import { Observable, Subject } from 'rxjs'; +import { CopyProjectDialogComponent } from '../modules/library/copy-project-dialog/copy-project-dialog.component'; +import { Course } from '../domain/course'; import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; +import { Injectable } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; +import { Observable, Subject } from 'rxjs'; import { Project } from '../domain/project'; -import { Teacher } from '../domain/teacher'; import { Run } from '../domain/run'; -import { Course } from '../domain/course'; -import { CopyProjectDialogComponent } from '../modules/library/copy-project-dialog/copy-project-dialog.component'; +import { Teacher } from '../domain/teacher'; import { TeacherRun } from './teacher-run'; @Injectable() @@ -82,6 +82,7 @@ export class TeacherService { createRun( projectId: number, periods: string, + isSurvey: boolean, maxStudentsPerTeam: number, startDate: number, endDate: number, @@ -91,6 +92,7 @@ export class TeacherService { let body = new HttpParams(); body = body.set('projectId', projectId + ''); body = body.set('periods', periods); + body = body.set('isSurvey', isSurvey); body = body.set('maxStudentsPerTeam', maxStudentsPerTeam + ''); body = body.set('startDate', startDate + ''); if (endDate) { diff --git a/src/assets/wise5/common/stepTools/step-tools.component.spec.ts b/src/assets/wise5/common/stepTools/step-tools.component.spec.ts index 320e0b7cafd..45e073c1275 100644 --- a/src/assets/wise5/common/stepTools/step-tools.component.spec.ts +++ b/src/assets/wise5/common/stepTools/step-tools.component.spec.ts @@ -1,13 +1,13 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ClassroomStatusService } from '../../services/classroomStatusService'; -import { TeacherDataService } from '../../services/teacherDataService'; -import { TeacherProjectService } from '../../services/teacherProjectService'; -import { TeacherWebSocketService } from '../../services/teacherWebSocketService'; -import { StepToolsComponent } from './step-tools.component'; -import { StudentTeacherCommonServicesModule } from '../../../../app/student-teacher-common-services.module'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; +import { StepToolsComponent } from './step-tools.component'; import { StudentNodeService } from '../../services/studentNodeService'; +import { StudentTeacherCommonServicesModule } from '../../../../app/student-teacher-common-services.module'; +import { TeacherDataService } from '../../services/teacherDataService'; +import { TeacherProjectService } from '../../services/teacherProjectService'; +import { TeacherWebSocketService } from '../../services/teacherWebSocketService'; describe('StepTools', () => { let component: StepToolsComponent; diff --git a/src/assets/wise5/common/stepTools/step-tools.component.ts b/src/assets/wise5/common/stepTools/step-tools.component.ts index f38f5f98420..c3aab33b425 100644 --- a/src/assets/wise5/common/stepTools/step-tools.component.ts +++ b/src/assets/wise5/common/stepTools/step-tools.component.ts @@ -1,19 +1,19 @@ +import { CommonModule } from '@angular/common'; import { Component, ViewEncapsulation } from '@angular/core'; import { Directionality } from '@angular/cdk/bidi'; -import { Subscription } from 'rxjs'; -import { NodeService } from '../../services/nodeService'; -import { TeacherDataService } from '../../services/teacherDataService'; -import { TeacherProjectService } from '../../services/teacherProjectService'; +import { FlexLayoutModule } from '@angular/flex-layout'; +import { FormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; -import { NodeIconComponent } from '../../vle/node-icon/node-icon.component'; -import { FlexLayoutModule } from '@angular/flex-layout'; import { MatTooltipModule } from '@angular/material/tooltip'; -import { CommonModule } from '@angular/common'; -import { FormsModule } from '@angular/forms'; -import { MatInputModule } from '@angular/material/input'; +import { NodeIconComponent } from '../../vle/node-icon/node-icon.component'; +import { NodeService } from '../../services/nodeService'; +import { Subscription } from 'rxjs'; +import { TeacherDataService } from '../../services/teacherDataService'; +import { TeacherProjectService } from '../../services/teacherProjectService'; @Component({ encapsulation: ViewEncapsulation.None, diff --git a/src/assets/wise5/themes/default/themeComponents/stepTools/step-tools.component.html b/src/assets/wise5/themes/default/themeComponents/stepTools/step-tools.component.html index aff929afab4..39c8473452e 100644 --- a/src/assets/wise5/themes/default/themeComponents/stepTools/step-tools.component.html +++ b/src/assets/wise5/themes/default/themeComponents/stepTools/step-tools.component.html @@ -12,7 +12,7 @@ {{ icons.prev }}
- +
  @@ -49,7 +49,7 @@
- +
+ @if (isSurvey) { +
+ +
+ }