diff --git a/package-lock.json b/package-lock.json index 4ee26757f7..0f8dabb348 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "wise", - "version": "5.20.4", + "version": "5.20.5", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 94f8210b6b..71c70cf53c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wise", - "version": "5.20.4", + "version": "5.20.5", "description": "Web-based Inquiry Science Environment", "main": "app.js", "browserslist": [ diff --git a/pom.xml b/pom.xml index b12ac1d1d8..d7c44772ce 100644 --- a/pom.xml +++ b/pom.xml @@ -34,7 +34,7 @@ <artifactId>wise</artifactId> <packaging>war</packaging> <name>Web-based Inquiry Science Environment</name> - <version>5.20.4</version> + <version>5.20.5</version> <url>http://wise5.org</url> <licenses> <license> diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIController.java b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIController.java index 79a5d8a107..72da0d69c3 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIController.java @@ -14,7 +14,6 @@ import org.apache.commons.lang3.RandomStringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.MessageSource; import org.springframework.security.access.annotation.Secured; import org.springframework.security.acls.model.Permission; import org.springframework.security.core.Authentication; @@ -38,7 +37,6 @@ import org.wise.portal.presentation.web.response.SimpleResponse; import org.wise.portal.service.authentication.DuplicateUsernameException; import org.wise.portal.service.authentication.UserDetailsService; -import org.wise.portal.service.mail.IMailFacade; /** * Teacher REST API @@ -55,12 +53,6 @@ public class TeacherAPIController extends UserAPIController { @Autowired private UserDetailsService userDetailsService; - @Autowired - protected IMailFacade mailService; - - @Autowired - protected MessageSource messageSource; - @Value("${google.clientId:}") private String googleClientId; diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/user/GoogleUserAPIController.java b/src/main/java/org/wise/portal/presentation/web/controllers/user/GoogleUserAPIController.java new file mode 100644 index 0000000000..d2cba48c11 --- /dev/null +++ b/src/main/java/org/wise/portal/presentation/web/controllers/user/GoogleUserAPIController.java @@ -0,0 +1,84 @@ +package org.wise.portal.presentation.web.controllers.user; + +import java.util.HashMap; +import java.util.Locale; + +import javax.mail.MessagingException; + +import org.springframework.security.access.annotation.Secured; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.wise.portal.domain.authentication.impl.PersistentUserDetails; +import org.wise.portal.domain.authentication.impl.TeacherUserDetails; +import org.wise.portal.domain.user.User; +import org.wise.portal.presentation.web.exception.InvalidPasswordExcpetion; + +@RestController +@RequestMapping("/api/google-user") +public class GoogleUserAPIController extends UserAPIController { + + @GetMapping("/check-user-exists") + boolean isGoogleIdExist(@RequestParam String googleUserId) { + return userService.retrieveUserByGoogleUserId(googleUserId) != null; + } + + @GetMapping("/check-user-matches") + boolean isGoogleIdMatches(@RequestParam String googleUserId, @RequestParam String userId) { + User user = userService.retrieveUserByGoogleUserId(googleUserId); + return user != null && user.getId().toString().equals(userId); + } + + @GetMapping("/get-user") + HashMap<String, Object> getUserByGoogleId(@RequestParam String googleUserId) { + User user = userService.retrieveUserByGoogleUserId(googleUserId); + HashMap<String, Object> response = new HashMap<String, Object>(); + if (user == null) { + response.put("status", "error"); + } else { + response.put("status", "success"); + response.put("userId", user.getId()); + response.put("username", user.getUserDetails().getUsername()); + response.put("firstName", user.getUserDetails().getFirstname()); + response.put("lastName", user.getUserDetails().getLastname()); + } + return response; + } + + @Secured("ROLE_USER") + @PostMapping("/unlink-account") + HashMap<String, Object> unlinkGoogleAccount(Authentication auth, @RequestParam String newPassword) + throws InvalidPasswordExcpetion { + if (newPassword.isEmpty()) { + throw new InvalidPasswordExcpetion(); + } + String username = auth.getName(); + User user = userService.retrieveUserByUsername(username); + ((PersistentUserDetails) user.getUserDetails()).setGoogleUserId(null); + userService.updateUserPassword(user, newPassword); + boolean isSendEmail = Boolean.parseBoolean(appProperties.getProperty("send_email_enabled", "false")); + if (isSendEmail && user.isTeacher()) { + this.sendUnlinkGoogleEmail((TeacherUserDetails) user.getUserDetails()); + } + return this.getUserInfo(auth, username); + } + + private void sendUnlinkGoogleEmail(TeacherUserDetails userDetails) { + String[] recipients = { userDetails.getEmailAddress() }; + String subject = messageSource.getMessage("unlink_google_account_success_email_subject", null, + "Successfully Unlinked Google Account", new Locale(userDetails.getLanguage())); + String username = userDetails.getUsername(); + String message = messageSource.getMessage("unlink_google_account_success_email_body", + new Object[]{username}, + "You have unlinked your Google account from WISE. To sign in to WISE in the future, please use your username and the password you just created. Your username is: " + username, + new Locale(userDetails.getLanguage())); + try { + mailService.postMail(recipients, subject, message, appProperties.getProperty("portalemailaddress")); + } catch (MessagingException e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/user/UserAPIController.java b/src/main/java/org/wise/portal/presentation/web/controllers/user/UserAPIController.java index b6ef0251f3..21673c81d5 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/user/UserAPIController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/user/UserAPIController.java @@ -12,6 +12,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.MessageSource; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.web.authentication.switchuser.SwitchUserFilter; @@ -63,6 +64,9 @@ public class UserAPIController { @Autowired protected IMailFacade mailService; + @Autowired + protected MessageSource messageSource; + @Value("${google.clientId:}") protected String googleClientId = ""; @@ -189,33 +193,6 @@ List<HashMap<String, String>> getSupportedLanguages() { return langs; } - @GetMapping("/check-google-user-exists") - boolean isGoogleIdExist(@RequestParam String googleUserId) { - return userService.retrieveUserByGoogleUserId(googleUserId) != null; - } - - @GetMapping("/check-google-user-matches") - boolean isGoogleIdMatches(@RequestParam String googleUserId, @RequestParam String userId) { - User user = userService.retrieveUserByGoogleUserId(googleUserId); - return user != null && user.getId().toString().equals(userId); - } - - @GetMapping("/google-user") - HashMap<String, Object> getUserByGoogleId(@RequestParam String googleUserId) { - User user = userService.retrieveUserByGoogleUserId(googleUserId); - HashMap<String, Object> response = new HashMap<String, Object>(); - if (user == null) { - response.put("status", "error"); - } else { - response.put("status", "success"); - response.put("userId", user.getId()); - response.put("username", user.getUserDetails().getUsername()); - response.put("firstName", user.getUserDetails().getFirstname()); - response.put("lastName", user.getUserDetails().getLastname()); - } - return response; - } - private String getLanguageName(String localeString) { if (localeString.toLowerCase().equals("zh_tw")) { return "Chinese (Traditional)"; diff --git a/src/main/java/org/wise/portal/presentation/web/exception/InvalidPasswordExcpetion.java b/src/main/java/org/wise/portal/presentation/web/exception/InvalidPasswordExcpetion.java new file mode 100644 index 0000000000..6d6dac6cbc --- /dev/null +++ b/src/main/java/org/wise/portal/presentation/web/exception/InvalidPasswordExcpetion.java @@ -0,0 +1,6 @@ +package org.wise.portal.presentation.web.exception; + +public class InvalidPasswordExcpetion extends Exception { + + private static final long serialVersionUID = 1L; +} diff --git a/src/main/resources/i18n/i18n.properties b/src/main/resources/i18n/i18n.properties index 7c050e8dcb..1ffd28e71c 100644 --- a/src/main/resources/i18n/i18n.properties +++ b/src/main/resources/i18n/i18n.properties @@ -308,6 +308,12 @@ teacher_cap.description=Text for the word "Teacher" team_cap=Team team_cap.description=Text for the word "Team" +unlink_google_account_success_email_subject=Successfully Unlinked Google Account +unlink_google_account_success_email_subject.description=Subject text in email to notify user about successfuly unlinking google account + +unlink_google_account_success_email_body=You have unlinked your Google account from WISE. To sign in to WISE in the future, please use your username and the password you just created.\n\nYour username is: {0}\n\nThank you for using WISE,\nWISE Team +unlink_google_account_success_email_body.description=Body text in email to notify user about successfully unlinking google account + # Root (/) Pages # accountmenu.forgot=Forgot Username or Password? diff --git a/src/main/resources/version.txt b/src/main/resources/version.txt index c523b9689b..fa20f9833e 100644 --- a/src/main/resources/version.txt +++ b/src/main/resources/version.txt @@ -1 +1 @@ -5.20.4 +5.20.5 diff --git a/src/main/webapp/site/src/app/modules/library/copy-project-dialog/copy-project-dialog.component.ts b/src/main/webapp/site/src/app/modules/library/copy-project-dialog/copy-project-dialog.component.ts index 8a2db347d5..5347591222 100644 --- a/src/main/webapp/site/src/app/modules/library/copy-project-dialog/copy-project-dialog.component.ts +++ b/src/main/webapp/site/src/app/modules/library/copy-project-dialog/copy-project-dialog.component.ts @@ -5,7 +5,6 @@ import { finalize } from 'rxjs/operators'; import { LibraryProject } from '../libraryProject'; import { LibraryService } from '../../../services/library.service'; import { MatSnackBar } from '@angular/material/snack-bar'; -import { Subscription } from 'rxjs'; @Component({ selector: 'app-copy-project-dialog', diff --git a/src/main/webapp/site/src/app/modules/shared/edit-password/edit-password.component.html b/src/main/webapp/site/src/app/modules/shared/edit-password/edit-password.component.html index 1a47a25674..e2fd57a2df 100644 --- a/src/main/webapp/site/src/app/modules/shared/edit-password/edit-password.component.html +++ b/src/main/webapp/site/src/app/modules/shared/edit-password/edit-password.component.html @@ -57,5 +57,17 @@ </div> </form> <ng-container *ngIf="isGoogleUser"> - <p class="notice" i18n>This account was created using Google and doesn't use a WISE password. If you would like to unlink your Google account, please <a routerLink="/contact">contact us</a>.</p> + <p fxLayoutAlign="start center" fxLayoutGap="8px" i18n> + <img class="google-icon" src="assets/img/icons/g-logo.png" i18n-alt alt="Google logo" /> + <span>This account was created using Google and doesn't use a WISE password.</span> + </p> + <p> + <button id="unlinkGoogleAccount" + class="unlink" + type="button" + mat-raised-button + (click)="unlinkGoogleAccount()"> + <span class="warn" i18n>Unlink Google Account</span> + </button> + </p> </ng-container> diff --git a/src/main/webapp/site/src/app/modules/shared/edit-password/edit-password.component.scss b/src/main/webapp/site/src/app/modules/shared/edit-password/edit-password.component.scss index 940482c43e..60fab472cc 100644 --- a/src/main/webapp/site/src/app/modules/shared/edit-password/edit-password.component.scss +++ b/src/main/webapp/site/src/app/modules/shared/edit-password/edit-password.component.scss @@ -11,6 +11,11 @@ form { } } -.notice { - margin: 0 auto; +.google-icon { + height: 1.8em; + width: auto; +} + +.unlink { + margin: 8px 0; } diff --git a/src/main/webapp/site/src/app/modules/shared/edit-password/edit-password.component.spec.ts b/src/main/webapp/site/src/app/modules/shared/edit-password/edit-password.component.spec.ts index 6d33a734a5..fd68f182c1 100644 --- a/src/main/webapp/site/src/app/modules/shared/edit-password/edit-password.component.spec.ts +++ b/src/main/webapp/site/src/app/modules/shared/edit-password/edit-password.component.spec.ts @@ -1,14 +1,17 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { EditPasswordComponent } from './edit-password.component'; import { UserService } from '../../../services/user.service'; -import { BehaviorSubject, Observable } from 'rxjs'; +import { BehaviorSubject, Observable, of } from 'rxjs'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { ReactiveFormsModule } from '@angular/forms'; -import { NO_ERRORS_SCHEMA, Provider } from '@angular/core'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { By } from '@angular/platform-browser'; import { User } from '../../../domain/user'; import { configureTestSuite } from 'ng-bullet'; +import { MatDialogModule } from '@angular/material/dialog'; +const CORRECT_OLD_PASS = 'a'; +const INCORRECT_OLD_PASS = 'b'; export class MockUserService { getUser(): BehaviorSubject<User> { @@ -22,13 +25,13 @@ export class MockUserService { } changePassword(username, oldPassword, newPassword) { - if (oldPassword === 'a') { - return Observable.create((observer) => { + if (oldPassword === CORRECT_OLD_PASS) { + return new Observable((observer) => { observer.next({ status: 'success', messageCode: 'passwordChanged' }); observer.complete(); }); } else { - return Observable.create((observer) => { + return new Observable((observer) => { observer.next({ status: 'error', messageCode: 'incorrectPassword' }); observer.complete(); }); @@ -36,22 +39,26 @@ export class MockUserService { } } -describe('EditPasswordComponent', () => { - let component: EditPasswordComponent; - let fixture: ComponentFixture<EditPasswordComponent>; +let component: EditPasswordComponent; +let fixture: ComponentFixture<EditPasswordComponent>; + +const getSubmitButton = () => { + return fixture.debugElement.nativeElement.querySelector('button[type="submit"]'); +}; - const getSubmitButton = () => { - return fixture.debugElement.nativeElement.querySelector('button[type="submit"]'); - }; +const getUnlinkGoogleAccountButton = () => { + return fixture.debugElement.nativeElement.querySelector('button[id="unlinkGoogleAccount"]'); +}; - const getForm = () => { - return fixture.debugElement.query(By.css('form')); - }; +const getForm = () => { + return fixture.debugElement.query(By.css('form')); +}; +describe('EditPasswordComponent', () => { configureTestSuite(() => { TestBed.configureTestingModule({ declarations: [EditPasswordComponent], - imports: [BrowserAnimationsModule, ReactiveFormsModule, MatSnackBarModule], + imports: [BrowserAnimationsModule, ReactiveFormsModule, MatSnackBarModule, MatDialogModule], providers: [{ provide: UserService, useValue: new MockUserService() }], schemas: [NO_ERRORS_SCHEMA] }); @@ -63,61 +70,60 @@ describe('EditPasswordComponent', () => { fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); - }); + initialState_disableSubmitButton(); + validForm_enableSubmitButton(); + passwordMismatch_disableSubmitButtonAndInvalidateForm(); + oldPasswordIncorrect_disableSubmitButtonAndShowError(); + formSubmit_disableSubmitButton(); + passwordChanged_handleResponse(); + incorrectPassword_showError(); + notGoogleUser_showUnlinkOption(); + unlinkGoogleButtonClick_showDialog(); +}); +function initialState_disableSubmitButton() { it('should disable submit button and invalidate form on initial state', () => { expect(component.changePasswordFormGroup.valid).toBeFalsy(); - const submitButton = getSubmitButton(); - expect(submitButton.disabled).toBe(true); + expectSubmitButtonDisabled(); }); +} +function validForm_enableSubmitButton() { it('should enable submit button when form is valid', () => { - component.changePasswordFormGroup.get('oldPassword').setValue('a'); - component.newPasswordFormGroup.get('newPassword').setValue('b'); - component.newPasswordFormGroup.get('confirmNewPassword').setValue('b'); - fixture.detectChanges(); - const submitButton = getSubmitButton(); - expect(submitButton.disabled).toBe(false); + setPasswords(CORRECT_OLD_PASS, 'b', 'b'); + expectSubmitButtonEnabled(); expect(component.changePasswordFormGroup.valid).toBeTruthy(); }); +} +function passwordMismatch_disableSubmitButtonAndInvalidateForm() { it('should disable submit button and invalidate form when new password and confirm new password fields do not match', () => { - component.changePasswordFormGroup.get('oldPassword').setValue('a'); - component.newPasswordFormGroup.get('newPassword').setValue('a'); - component.newPasswordFormGroup.get('confirmNewPassword').setValue('b'); - fixture.detectChanges(); - const submitButton = getSubmitButton(); - expect(submitButton.disabled).toBe(true); + setPasswords(CORRECT_OLD_PASS, 'a', 'b'); + expectSubmitButtonDisabled(); expect(component.changePasswordFormGroup.valid).toBeFalsy(); }); +} +function oldPasswordIncorrect_disableSubmitButtonAndShowError() { it('should disable submit button and set incorrectPassword error when old password is incorrect', async () => { - component.changePasswordFormGroup.get('oldPassword').setValue('b'); - component.newPasswordFormGroup.get('newPassword').setValue('c'); - component.newPasswordFormGroup.get('confirmNewPassword').setValue('c'); - const form = getForm(); - form.triggerEventHandler('submit', null); - fixture.detectChanges(); - const submitButton = getSubmitButton(); - expect(submitButton.disabled).toBe(true); + setPasswords(INCORRECT_OLD_PASS, 'c', 'c'); + submitForm(); + expectSubmitButtonDisabled(); expect(component.changePasswordFormGroup.get('oldPassword').getError('incorrectPassword')).toBe( true ); }); +} +function formSubmit_disableSubmitButton() { it('should disable submit button when form is successfully submitted', async () => { - component.changePasswordFormGroup.get('oldPassword').setValue('a'); - component.newPasswordFormGroup.get('newPassword').setValue('b'); - component.newPasswordFormGroup.get('confirmNewPassword').setValue('b'); - const form = getForm(); - form.triggerEventHandler('submit', null); - fixture.detectChanges(); - const submitButton = getSubmitButton(); - expect(submitButton.disabled).toBe(true); + setPasswords(CORRECT_OLD_PASS, 'b', 'b'); + submitForm(); + expectSubmitButtonDisabled(); }); +} +function passwordChanged_handleResponse() { it('should handle the change password response when the password was successfully changed', () => { const resetFormSpy = spyOn(component, 'resetForm'); const snackBarSpy = spyOn(component.snackBar, 'open'); @@ -129,7 +135,9 @@ describe('EditPasswordComponent', () => { expect(resetFormSpy).toHaveBeenCalled(); expect(snackBarSpy).toHaveBeenCalled(); }); +} +function incorrectPassword_showError() { it('should handle the change password response when the password was incorrect', () => { const response = { status: 'error', @@ -140,4 +148,44 @@ describe('EditPasswordComponent', () => { true ); }); -}); +} + +function notGoogleUser_showUnlinkOption() { + it('should hide show option to unlink google account if the user is not a google user', () => { + expect(getUnlinkGoogleAccountButton()).toBeNull(); + }); +} + +function unlinkGoogleButtonClick_showDialog() { + it('clicking on unlink google account link should open a dialog', () => { + const dialogSpy = spyOn(component.dialog, 'open'); + setGoogleUser(); + getUnlinkGoogleAccountButton().click(); + expect(dialogSpy).toHaveBeenCalled(); + }); +} + +export function expectSubmitButtonDisabled() { + expect(getSubmitButton().disabled).toBe(true); +} + +function expectSubmitButtonEnabled() { + expect(getSubmitButton().disabled).toBe(false); +} + +function submitForm() { + getForm().triggerEventHandler('submit', null); + fixture.detectChanges(); +} + +function setGoogleUser() { + component.isGoogleUser = true; + fixture.detectChanges(); +} + +function setPasswords(oldPass: string, newPass: string, newPassConfirm: string) { + component.changePasswordFormGroup.get('oldPassword').setValue(oldPass); + component.newPasswordFormGroup.get('newPassword').setValue(newPass); + component.newPasswordFormGroup.get('confirmNewPassword').setValue(newPassConfirm); + fixture.detectChanges(); +} diff --git a/src/main/webapp/site/src/app/modules/shared/edit-password/edit-password.component.ts b/src/main/webapp/site/src/app/modules/shared/edit-password/edit-password.component.ts index f22788cc5f..7539bc803e 100644 --- a/src/main/webapp/site/src/app/modules/shared/edit-password/edit-password.component.ts +++ b/src/main/webapp/site/src/app/modules/shared/edit-password/edit-password.component.ts @@ -1,15 +1,18 @@ -import { Component, OnInit, ViewChild } from '@angular/core'; +import { Component, ViewChild } from '@angular/core'; import { FormControl, FormGroup, Validators, FormBuilder } from '@angular/forms'; import { finalize } from 'rxjs/operators'; +import { MatDialog } from '@angular/material/dialog'; import { MatSnackBar } from '@angular/material/snack-bar'; import { UserService } from '../../../services/user.service'; +import { UnlinkGoogleAccountConfirmComponent } from '../unlink-google-account-confirm/unlink-google-account-confirm.component'; +import { passwordMatchValidator } from '../validators/password-match.validator'; @Component({ selector: 'app-edit-password', templateUrl: './edit-password.component.html', styleUrls: ['./edit-password.component.scss'] }) -export class EditPasswordComponent implements OnInit { +export class EditPasswordComponent { @ViewChild('changePasswordForm', { static: false }) changePasswordForm; isSaving: boolean = false; isGoogleUser: boolean = false; @@ -19,7 +22,7 @@ export class EditPasswordComponent implements OnInit { newPassword: new FormControl('', [Validators.required]), confirmNewPassword: new FormControl('', [Validators.required]) }, - { validator: this.passwordMatchValidator } + { validator: passwordMatchValidator } ); changePasswordFormGroup: FormGroup = this.fb.group({ @@ -30,6 +33,7 @@ export class EditPasswordComponent implements OnInit { constructor( private fb: FormBuilder, private userService: UserService, + public dialog: MatDialog, public snackBar: MatSnackBar ) {} @@ -39,18 +43,6 @@ export class EditPasswordComponent implements OnInit { }); } - passwordMatchValidator(passwordsFormGroup: FormGroup) { - const newPassword = passwordsFormGroup.get('newPassword').value; - const confirmNewPassword = passwordsFormGroup.get('confirmNewPassword').value; - if (newPassword === confirmNewPassword) { - return null; - } else { - const error = { passwordDoesNotMatch: true }; - passwordsFormGroup.controls['confirmNewPassword'].setErrors(error); - return error; - } - } - saveChanges() { this.isSaving = true; const oldPassword: string = this.getControlFieldValue('oldPassword'); @@ -90,6 +82,12 @@ export class EditPasswordComponent implements OnInit { } } + unlinkGoogleAccount() { + this.dialog.open(UnlinkGoogleAccountConfirmComponent, { + panelClass: 'mat-dialog--sm' + }); + } + resetForm() { this.changePasswordForm.resetForm(); } diff --git a/src/main/webapp/site/src/app/modules/shared/shared.module.ts b/src/main/webapp/site/src/app/modules/shared/shared.module.ts index 5ec6a150df..807f04e4e7 100644 --- a/src/main/webapp/site/src/app/modules/shared/shared.module.ts +++ b/src/main/webapp/site/src/app/modules/shared/shared.module.ts @@ -5,6 +5,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; +import { MatDialogModule } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; @@ -13,6 +14,7 @@ import { MatSelectModule } from '@angular/material/select'; const materialModules = [ MatButtonModule, MatCardModule, + MatDialogModule, MatIconModule, MatInputModule, MatFormFieldModule, @@ -26,6 +28,9 @@ import { HeroSectionComponent } from './hero-section/hero-section.component'; import { SearchBarComponent } from './search-bar/search-bar.component'; import { SelectMenuComponent } from './select-menu/select-menu.component'; import { EditPasswordComponent } from './edit-password/edit-password.component'; +import { UnlinkGoogleAccountConfirmComponent } from './unlink-google-account-confirm/unlink-google-account-confirm.component'; +import { UnlinkGoogleAccountPasswordComponent } from './unlink-google-account-password/unlink-google-account-password.component'; +import { UnlinkGoogleAccountSuccessComponent } from './unlink-google-account-success/unlink-google-account-success.component'; @NgModule({ imports: [ @@ -52,7 +57,10 @@ import { EditPasswordComponent } from './edit-password/edit-password.component'; HeroSectionComponent, SearchBarComponent, SelectMenuComponent, - EditPasswordComponent + EditPasswordComponent, + UnlinkGoogleAccountConfirmComponent, + UnlinkGoogleAccountPasswordComponent, + UnlinkGoogleAccountSuccessComponent ] }) export class SharedModule {} diff --git a/src/main/webapp/site/src/app/modules/shared/unlink-google-account-confirm/unlink-google-account-confirm.component.html b/src/main/webapp/site/src/app/modules/shared/unlink-google-account-confirm/unlink-google-account-confirm.component.html new file mode 100644 index 0000000000..9fd9a47c13 --- /dev/null +++ b/src/main/webapp/site/src/app/modules/shared/unlink-google-account-confirm/unlink-google-account-confirm.component.html @@ -0,0 +1,16 @@ +<h2 class="mat-dialog-title" fxLayoutAlign="start center" fxLayoutGap="8px"> + <img class="google-icon" src="assets/img/icons/g-logo.png" i18n-alt alt="Google logo" /> + <span i18n>Unlink Google Account</span> + <span fxFlex></span> + <mat-icon color="warn" i18n-aria-label aria-label="Warning">warning</mat-icon> +</h2> +<mat-dialog-content> + <div class="info-block"> + <p i18n>To remove the link to your Google account, you will be asked to create a WISE password. In the future, you'll sign in to WISE using your username and password.</p> + <p><strong i18n>You will no longer be able to sign in to WISE using Google. Would you like to continue?</strong></p> + </div> +</mat-dialog-content> +<mat-dialog-actions fxLayout="row" fxLayoutAlign="end"> + <button mat-flat-button color="primary" mat-dialog-close i18n>Cancel</button> + <a mat-button color="warn" (click)="continue()" i18n>Continue</a> +</mat-dialog-actions> diff --git a/src/main/webapp/site/src/app/modules/shared/unlink-google-account-confirm/unlink-google-account-confirm.component.scss b/src/main/webapp/site/src/app/modules/shared/unlink-google-account-confirm/unlink-google-account-confirm.component.scss new file mode 100644 index 0000000000..257ab6ea45 --- /dev/null +++ b/src/main/webapp/site/src/app/modules/shared/unlink-google-account-confirm/unlink-google-account-confirm.component.scss @@ -0,0 +1,4 @@ +.google-icon { + height: 1.4em; + width: auto; +} \ No newline at end of file diff --git a/src/main/webapp/site/src/app/modules/shared/unlink-google-account-confirm/unlink-google-account-confirm.component.spec.ts b/src/main/webapp/site/src/app/modules/shared/unlink-google-account-confirm/unlink-google-account-confirm.component.spec.ts new file mode 100644 index 0000000000..400a72a901 --- /dev/null +++ b/src/main/webapp/site/src/app/modules/shared/unlink-google-account-confirm/unlink-google-account-confirm.component.spec.ts @@ -0,0 +1,37 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialogModule } from '@angular/material/dialog'; +import { configureTestSuite } from 'ng-bullet'; +import { UnlinkGoogleAccountConfirmComponent } from './unlink-google-account-confirm.component'; + +let component: UnlinkGoogleAccountConfirmComponent; +let fixture: ComponentFixture<UnlinkGoogleAccountConfirmComponent>; + +describe('UnlinkGoogleAccountConfirmComponent', () => { + configureTestSuite(() => { + TestBed.configureTestingModule({ + declarations: [UnlinkGoogleAccountConfirmComponent], + imports: [MatDialogModule], + providers: [], + schemas: [NO_ERRORS_SCHEMA] + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(UnlinkGoogleAccountConfirmComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + continue_closeAllDialogsAndOpenChangePasswordDialog(); +}); + +function continue_closeAllDialogsAndOpenChangePasswordDialog() { + it('continue() should closeAllDialogs and open a new dialog to edit password', () => { + const closeAllDialogSpy = spyOn(component.dialog, 'closeAll'); + const openDialogSpy = spyOn(component.dialog, 'open'); + component.continue(); + expect(closeAllDialogSpy).toHaveBeenCalled(); + expect(openDialogSpy).toHaveBeenCalled(); + }); +} diff --git a/src/main/webapp/site/src/app/modules/shared/unlink-google-account-confirm/unlink-google-account-confirm.component.ts b/src/main/webapp/site/src/app/modules/shared/unlink-google-account-confirm/unlink-google-account-confirm.component.ts new file mode 100644 index 0000000000..99a4fe2fd8 --- /dev/null +++ b/src/main/webapp/site/src/app/modules/shared/unlink-google-account-confirm/unlink-google-account-confirm.component.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { UnlinkGoogleAccountPasswordComponent } from '../unlink-google-account-password/unlink-google-account-password.component'; + +@Component({ + styleUrls: ['./unlink-google-account-confirm.component.scss'], + templateUrl: './unlink-google-account-confirm.component.html' +}) +export class UnlinkGoogleAccountConfirmComponent { + constructor(public dialog: MatDialog) {} + + continue() { + this.dialog.closeAll(); + this.dialog.open(UnlinkGoogleAccountPasswordComponent, { + panelClass: 'mat-dialog--sm' + }); + } +} diff --git a/src/main/webapp/site/src/app/modules/shared/unlink-google-account-password/unlink-google-account-password.component.html b/src/main/webapp/site/src/app/modules/shared/unlink-google-account-password/unlink-google-account-password.component.html new file mode 100644 index 0000000000..e5c496120f --- /dev/null +++ b/src/main/webapp/site/src/app/modules/shared/unlink-google-account-password/unlink-google-account-password.component.html @@ -0,0 +1,41 @@ +<h2 class="mat-dialog-title" fxLayoutAlign="start center" fxLayoutGap="8px"> + <img class="google-icon" src="assets/img/icons/g-logo.png" i18n-alt alt="Google logo" /> + <span i18n>Unlink Google Account</span> +</h2> +<form [formGroup]="newPasswordFormGroup"> + <mat-dialog-content class="mat-dialog-content--scroll" fxLayout="column"> + <h3 i18n>Create a WISE password:</h3> + <mat-form-field appearance="fill"> + <mat-label i18n>New Password</mat-label> + <input matInput + id="newPassword" + type="password" + name="newPassword" + formControlName="newPassword" + required /> + <mat-error *ngIf="newPasswordFormGroup.controls['newPassword'].hasError('required')" i18n>New Password required</mat-error> + </mat-form-field> + <mat-form-field appearance="fill"> + <mat-label i18n>Confirm New Password</mat-label> + <input matInput + id="confirmNewPassword" + type="password" + name="confirmNewPassword" + formControlName="confirmNewPassword" + required /> + <mat-error *ngIf="newPasswordFormGroup.controls['confirmNewPassword'].hasError('required')" i18n>Confirm Password required</mat-error> + <mat-error *ngIf="newPasswordFormGroup.hasError('passwordDoesNotMatch')" i18n>Passwords do not match</mat-error> + </mat-form-field> + </mat-dialog-content> + <mat-dialog-actions fxLayoutAlign="end"> + <button mat-button mat-dialog-close i18n>Cancel</button> + <button mat-raised-button + color="primary" + type="submit" + [disabled]="!newPasswordFormGroup.valid || isSaving" + (click)="submit()"> + <ng-container i18n>Submit</ng-container> + <mat-progress-bar mode="indeterminate" *ngIf="isSaving"></mat-progress-bar> + </button> + </mat-dialog-actions> +</form> diff --git a/src/main/webapp/site/src/app/modules/shared/unlink-google-account-password/unlink-google-account-password.component.scss b/src/main/webapp/site/src/app/modules/shared/unlink-google-account-password/unlink-google-account-password.component.scss new file mode 100644 index 0000000000..6629b7fe81 --- /dev/null +++ b/src/main/webapp/site/src/app/modules/shared/unlink-google-account-password/unlink-google-account-password.component.scss @@ -0,0 +1,4 @@ +.google-icon { + height: 1.4em; + width: auto; +} diff --git a/src/main/webapp/site/src/app/modules/shared/unlink-google-account-password/unlink-google-account-password.component.spec.ts b/src/main/webapp/site/src/app/modules/shared/unlink-google-account-password/unlink-google-account-password.component.spec.ts new file mode 100644 index 0000000000..d10a68730e --- /dev/null +++ b/src/main/webapp/site/src/app/modules/shared/unlink-google-account-password/unlink-google-account-password.component.spec.ts @@ -0,0 +1,51 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { MatDialogModule } from '@angular/material/dialog'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { configureTestSuite } from 'ng-bullet'; +import { Subscription } from 'rxjs'; +import { UserService } from '../../../services/user.service'; +import { UnlinkGoogleAccountPasswordComponent } from './unlink-google-account-password.component'; + +class MockUserService { + unlinkGoogleUser(newPassword: string) { + return new Subscription(); + } +} + +let component: UnlinkGoogleAccountPasswordComponent; +let fixture: ComponentFixture<UnlinkGoogleAccountPasswordComponent>; +let userService = new MockUserService(); + +describe('UnlinkGoogleAccountPasswordComponent', () => { + configureTestSuite(() => { + TestBed.configureTestingModule({ + declarations: [UnlinkGoogleAccountPasswordComponent], + imports: [BrowserAnimationsModule, ReactiveFormsModule, MatDialogModule], + providers: [{ provide: UserService, useValue: userService }], + schemas: [NO_ERRORS_SCHEMA] + }); + }); + beforeEach(() => { + fixture = TestBed.createComponent(UnlinkGoogleAccountPasswordComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + formSubmit_callUserServiceUnlinkGoogleUserFunction(); +}); + +function formSubmit_callUserServiceUnlinkGoogleUserFunction() { + it('should call UserService.UnlinkGoogleUserFunction when form is submitted', () => { + const unlinkFunctionSpy = spyOn(userService, 'unlinkGoogleUser').and.returnValue( + new Subscription() + ); + const newPassword = 'aloha'; + component.newPasswordFormGroup.setValue({ + newPassword: newPassword, + confirmNewPassword: newPassword + }); + component.submit(); + expect(unlinkFunctionSpy).toHaveBeenCalledWith(newPassword); + }); +} diff --git a/src/main/webapp/site/src/app/modules/shared/unlink-google-account-password/unlink-google-account-password.component.ts b/src/main/webapp/site/src/app/modules/shared/unlink-google-account-password/unlink-google-account-password.component.ts new file mode 100644 index 0000000000..da0a00f3a0 --- /dev/null +++ b/src/main/webapp/site/src/app/modules/shared/unlink-google-account-password/unlink-google-account-password.component.ts @@ -0,0 +1,40 @@ +import { Component } from '@angular/core'; +import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { UserService } from '../../../services/user.service'; +import { UnlinkGoogleAccountSuccessComponent } from '../unlink-google-account-success/unlink-google-account-success.component'; +import { passwordMatchValidator } from '../validators/password-match.validator'; + +@Component({ + styleUrls: ['./unlink-google-account-password.component.scss'], + templateUrl: './unlink-google-account-password.component.html' +}) +export class UnlinkGoogleAccountPasswordComponent { + isSaving: boolean = false; + newPasswordFormGroup: FormGroup = this.fb.group( + { + newPassword: new FormControl('', [Validators.required]), + confirmNewPassword: new FormControl('', [Validators.required]) + }, + { validator: passwordMatchValidator } + ); + + constructor( + private fb: FormBuilder, + public dialog: MatDialog, + private userService: UserService + ) {} + + submit() { + this.isSaving = true; + this.userService + .unlinkGoogleUser(this.newPasswordFormGroup.get('newPassword').value) + .add(() => { + this.isSaving = false; + this.dialog.closeAll(); + this.dialog.open(UnlinkGoogleAccountSuccessComponent, { + panelClass: 'mat-dialog--sm' + }); + }); + } +} diff --git a/src/main/webapp/site/src/app/modules/shared/unlink-google-account-success/unlink-google-account-success.component.html b/src/main/webapp/site/src/app/modules/shared/unlink-google-account-success/unlink-google-account-success.component.html new file mode 100644 index 0000000000..c9d91706f6 --- /dev/null +++ b/src/main/webapp/site/src/app/modules/shared/unlink-google-account-success/unlink-google-account-success.component.html @@ -0,0 +1,13 @@ +<h2 class="mat-dialog-title" fxLayoutAlign="start center" fxLayoutGap="8px"> + <img class="google-icon" src="assets/img/icons/g-logo.png" i18n-alt alt="Google logo" /> + <span i18n>Unlink Google Account</span> +</h2> +<mat-dialog-content> + <div class="info-block"> + <p i18n>Success! You have unlinked your Google account from WISE. To sign in to WISE in the future, please use your username and the password you just created.</p> + <p i18n>Your username is: <span class="mat-body-2">{{ username }}</span>.</p> + </div> +</mat-dialog-content> +<mat-dialog-actions fxLayoutAlign="end"> + <button mat-button mat-dialog-close i18n>Done</button> +</mat-dialog-actions> diff --git a/src/main/webapp/site/src/app/modules/shared/unlink-google-account-success/unlink-google-account-success.component.scss b/src/main/webapp/site/src/app/modules/shared/unlink-google-account-success/unlink-google-account-success.component.scss new file mode 100644 index 0000000000..6629b7fe81 --- /dev/null +++ b/src/main/webapp/site/src/app/modules/shared/unlink-google-account-success/unlink-google-account-success.component.scss @@ -0,0 +1,4 @@ +.google-icon { + height: 1.4em; + width: auto; +} diff --git a/src/main/webapp/site/src/app/modules/shared/unlink-google-account-success/unlink-google-account-success.component.ts b/src/main/webapp/site/src/app/modules/shared/unlink-google-account-success/unlink-google-account-success.component.ts new file mode 100644 index 0000000000..1701865273 --- /dev/null +++ b/src/main/webapp/site/src/app/modules/shared/unlink-google-account-success/unlink-google-account-success.component.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; +import { Teacher } from '../../../domain/teacher'; +import { UserService } from '../../../services/user.service'; + +@Component({ + styleUrls: ['unlink-google-account-success.component.scss'], + templateUrl: 'unlink-google-account-success.component.html' +}) +export class UnlinkGoogleAccountSuccessComponent { + username: string; + + constructor(private userService: UserService) {} + + ngOnInit() { + const user = <Teacher>this.userService.getUser().getValue(); + this.username = user.username; + } +} diff --git a/src/main/webapp/site/src/app/modules/shared/validators/password-match.validator.ts b/src/main/webapp/site/src/app/modules/shared/validators/password-match.validator.ts new file mode 100644 index 0000000000..89ac014720 --- /dev/null +++ b/src/main/webapp/site/src/app/modules/shared/validators/password-match.validator.ts @@ -0,0 +1,13 @@ +import { FormGroup } from '@angular/forms'; + +export function passwordMatchValidator(passwordsFormGroup: FormGroup) { + const newPassword = passwordsFormGroup.get('newPassword').value; + const confirmNewPassword = passwordsFormGroup.get('confirmNewPassword').value; + if (newPassword === confirmNewPassword) { + return null; + } else { + const error = { passwordDoesNotMatch: true }; + passwordsFormGroup.controls['confirmNewPassword'].setErrors(error); + return error; + } +} diff --git a/src/main/webapp/site/src/app/services/milestoneService.spec.ts b/src/main/webapp/site/src/app/services/milestoneService.spec.ts index a9d7ec91b9..69177122c3 100644 --- a/src/main/webapp/site/src/app/services/milestoneService.spec.ts +++ b/src/main/webapp/site/src/app/services/milestoneService.spec.ts @@ -48,6 +48,10 @@ const aggregateAutoScoresSample = { const possibleScoresKi = [1, 2, 3, 4, 5]; +const sampleAggregateData = { + counts: createScoreCounts([10, 20, 30, 40, 50]) +}; + const reportSettingsCustomScoreValuesSample = { customScoreValues: { ki: [1, 2, 3, 4] @@ -964,205 +968,39 @@ function isPercentOfScoresNotEqualTo() { function getComparatorSum() { describe('getComparatorSum()', () => { - getGreaterThanSum(); - getGreaterThanOrEqualToSum(); - getLessThanSum(); - getEqualToSum(); - getNotEqualToSum(); - }); -} - -function getGreaterThanSum() { - const aggregateData = { - counts: createScoreCounts([10, 20, 30, 40, 50]) - }; - it('should get greater than sum with score 1', () => { - const satisfyCriterion = { value: 1 }; - expect( - service.getComparatorSum( - satisfyCriterion, - aggregateData, - possibleScoresKi, - utilService.greaterThan - ) - ).toEqual(140); - }); - it('should get greater than sum with score 2', () => { - const satisfyCriterion = { value: 2 }; - expect( - service.getComparatorSum( - satisfyCriterion, - aggregateData, - possibleScoresKi, - utilService.greaterThan - ) - ).toEqual(120); - }); - it('should get greater than sum with score 3', () => { - const satisfyCriterion = { value: 3 }; - expect( - service.getComparatorSum( - satisfyCriterion, - aggregateData, - possibleScoresKi, - utilService.greaterThan - ) - ).toEqual(90); - }); - it('should get greater than sum with score 4', () => { - const satisfyCriterion = { value: 4 }; - expect( - service.getComparatorSum( - satisfyCriterion, - aggregateData, - possibleScoresKi, - utilService.greaterThan - ) - ).toEqual(50); + getComparatorSum_greaterThan0_ReturnSumAll(); + getComparatorSum_greaterThan3_ReturnSumPartial(); + getComparatorSum_greaterThan5_Return0(); }); } -function getGreaterThanOrEqualToSum() { - const aggregateData = { - counts: createScoreCounts([10, 20, 30, 40, 50]) - }; - it('should get greater than or equal to sum with score 1', () => { - const satisfyCriterion = { value: 1 }; - expect( - service.getComparatorSum( - satisfyCriterion, - aggregateData, - possibleScoresKi, - utilService.greaterThanEqualTo - ) - ).toEqual(150); - }); - it('should get greater than or equal to sum with score 2', () => { - const satisfyCriterion = { value: 2 }; - expect( - service.getComparatorSum( - satisfyCriterion, - aggregateData, - possibleScoresKi, - utilService.greaterThanEqualTo - ) - ).toEqual(140); - }); - it('should get greater than or equal to sum with score 3', () => { - const satisfyCriterion = { value: 3 }; - expect( - service.getComparatorSum( - satisfyCriterion, - aggregateData, - possibleScoresKi, - utilService.greaterThanEqualTo - ) - ).toEqual(120); - }); - it('should get greater than or equal to sum with score 4', () => { - const satisfyCriterion = { value: 4 }; - expect( - service.getComparatorSum( - satisfyCriterion, - aggregateData, - possibleScoresKi, - utilService.greaterThanEqualTo - ) - ).toEqual(90); - }); - it('should get greater than or equal to sum with score 5', () => { - const satisfyCriterion = { value: 5 }; - expect( - service.getComparatorSum( - satisfyCriterion, - aggregateData, - possibleScoresKi, - utilService.greaterThanEqualTo - ) - ).toEqual(50); - }); +function expectComparatorResult(satisfyCriterionValue: number, expectedResult: number) { + const satisfyCriterion = { value: satisfyCriterionValue }; + expect( + service.getComparatorSum( + satisfyCriterion, + sampleAggregateData, + possibleScoresKi, + utilService.greaterThan + ) + ).toEqual(expectedResult); } -function getLessThanSum() { - const aggregateData = { - counts: createScoreCounts([10, 20, 30, 40, 50]) - }; - it('should get less than sum with score 2', () => { - const satisfyCriterion = { value: 2 }; - expect( - service.getComparatorSum( - satisfyCriterion, - aggregateData, - possibleScoresKi, - utilService.lessThan - ) - ).toEqual(10); - }); - it('should get less than sum with score 3', () => { - const satisfyCriterion = { value: 3 }; - expect( - service.getComparatorSum( - satisfyCriterion, - aggregateData, - possibleScoresKi, - utilService.lessThan - ) - ).toEqual(30); - }); - it('should get less than sum with score 4', () => { - const satisfyCriterion = { value: 4 }; - expect( - service.getComparatorSum( - satisfyCriterion, - aggregateData, - possibleScoresKi, - utilService.lessThan - ) - ).toEqual(60); - }); - it('should get less than sum with score 5', () => { - const satisfyCriterion = { value: 5 }; - expect( - service.getComparatorSum( - satisfyCriterion, - aggregateData, - possibleScoresKi, - utilService.lessThan - ) - ).toEqual(100); +function getComparatorSum_greaterThan0_ReturnSumAll() { + it('should get greater than sum with score 0', () => { + expectComparatorResult(0, 150); }); } -function getEqualToSum() { - it('should return the sum of scores equal to value', () => { - const satisfyCriterion = { value: 3 }; - const aggregateData = { - counts: createScoreCounts([10, 20, 30, 40, 50]) - }; - expect( - service.getComparatorSum( - satisfyCriterion, - aggregateData, - possibleScoresKi, - utilService.equalTo - ) - ).toEqual(30); +function getComparatorSum_greaterThan3_ReturnSumPartial() { + it('should get greater than sum with score 3', () => { + expectComparatorResult(3, 90); }); } -function getNotEqualToSum() { - const aggregateData = { - counts: { 1: 2, 2: 0, 3: 1, 4: 0, 5: 0 }, - scoreCount: 3 - }; - it('should return the sum of scores not equal to value', () => { - const result = service.getComparatorSum( - satisfyCriterionSample, - aggregateData, - possibleScoresKi, - utilService.notEqualTo - ); - expect(result).toBe(2); +function getComparatorSum_greaterThan5_Return0() { + it('should get greater than sum with score 5', () => { + expectComparatorResult(5, 0); }); } diff --git a/src/main/webapp/site/src/app/services/user.service.spec.ts b/src/main/webapp/site/src/app/services/user.service.spec.ts index 5cb27e9c91..8671e76ce5 100644 --- a/src/main/webapp/site/src/app/services/user.service.spec.ts +++ b/src/main/webapp/site/src/app/services/user.service.spec.ts @@ -1,8 +1,10 @@ -import { TestBed, inject } from '@angular/core/testing'; +import { fakeAsync, TestBed, tick } from '@angular/core/testing'; import { UserService } from './user.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { ConfigService } from './config.service'; +let service: UserService; +let http: HttpTestingController; export class MockConfigService {} describe('UserService', () => { @@ -11,9 +13,21 @@ describe('UserService', () => { providers: [UserService, { provide: ConfigService, useClass: MockConfigService }], imports: [HttpClientTestingModule] }); + service = TestBed.inject(UserService); + http = TestBed.inject(HttpTestingController); }); + unlinkGoogleAccount_postToUrl(); +}); - it('should be created', inject([UserService, ConfigService], (service: UserService) => { - expect(service).toBeTruthy(); +function unlinkGoogleAccount_postToUrl() { + it('unlinkGoogleAccount() should make POST request to unlink google account', fakeAsync(() => { + const newPassword = 'my new pass'; + service.unlinkGoogleUser(newPassword); + const unlinkRequest = http.expectOne({ + url: '/api/google-user/unlink-account', + method: 'POST' + }); + unlinkRequest.flush({ response: 'success' }); + tick(); })); -}); +} diff --git a/src/main/webapp/site/src/app/services/user.service.ts b/src/main/webapp/site/src/app/services/user.service.ts index 65d7695e52..b419803a08 100644 --- a/src/main/webapp/site/src/app/services/user.service.ts +++ b/src/main/webapp/site/src/app/services/user.service.ts @@ -12,13 +12,14 @@ import { Student } from '../domain/student'; export class UserService { private userUrl = '/api/user/info'; private user$: BehaviorSubject<User> = new BehaviorSubject<User>(null); - private checkGoogleUserExistsUrl = '/api/user/check-google-user-exists'; - private checkGoogleUserMatchesUrl = '/api/user/check-google-user-matches'; - private googleUserUrl = '/api/user/google-user'; + private checkGoogleUserExistsUrl = '/api/google-user/check-user-exists'; + private checkGoogleUserMatchesUrl = '/api/google-user/check-user-matches'; + private googleUserUrl = '/api/google-user/get-user'; private checkAuthenticationUrl = '/api/user/check-authentication'; private changePasswordUrl = '/api/user/password'; private languagesUrl = '/api/user/languages'; private contactUrl = '/api/contact'; + private unlinkGoogleAccountUrl = '/api/google-user/unlink-account'; isAuthenticated = false; isRecaptchaRequired = false; redirectUrl: string; // redirect here after logging in @@ -126,6 +127,17 @@ export class UserService { return this.http.get<User>(this.checkGoogleUserMatchesUrl, { params: params }); } + unlinkGoogleUser(newPassword: string) { + 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<any>(this.unlinkGoogleAccountUrl, body, { headers: headers }) + .subscribe((user) => { + this.user$.next(user); + }); + } + getUserByGoogleId(googleUserId: string) { let params = new HttpParams(); params = params.set('googleUserId', googleUserId); diff --git a/src/main/webapp/site/src/app/student/account/edit-profile/edit-profile.component.html b/src/main/webapp/site/src/app/student/account/edit-profile/edit-profile.component.html index e69a78b318..10dd31c105 100644 --- a/src/main/webapp/site/src/app/student/account/edit-profile/edit-profile.component.html +++ b/src/main/webapp/site/src/app/student/account/edit-profile/edit-profile.component.html @@ -1,5 +1,5 @@ <form role="form" (submit)="saveChanges()" [formGroup]="editProfileFormGroup"> - <div fxLayout="column" fxLayoutAlign="start"> + <div class="inputs"> <p> <mat-form-field fxFlex appearance="fill"> <mat-label i18n>First Name</mat-label> @@ -42,15 +42,30 @@ <mat-error *ngIf="editProfileFormGroup.controls['language'].hasError('required')" i18n>Language required</mat-error> </mat-form-field> </p> - <div> - <button mat-raised-button - color="primary" - type="submit" - [disabled]="!editProfileFormGroup.valid || !changed || isSaving" - fxFlex - fxFlex.gt-xs="0 0 auto"> - <mat-progress-bar mode="indeterminate" *ngIf="isSaving"></mat-progress-bar> - <ng-container i18n>Save Changes</ng-container> + </div> + <div class="actions" + fxLayout="column" + fxLayout.gt-sm="row" + fxLayoutAlign="center start" + fxLayoutAlign.gt-sm="start center" + fxLayoutGap="24px"> + <button mat-raised-button + color="primary" + type="submit" + [disabled]="!editProfileFormGroup.valid || !changed || isSaving"> + <mat-progress-bar mode="indeterminate" *ngIf="isSaving"></mat-progress-bar> + <ng-container i18n>Save Changes</ng-container> + </button> + <div *ngIf="isGoogleUser" + fxLayout="row wrap" + fxLayoutAlign="start center" + fxLayoutGap="8px"> + <div fxLayoutAlign="start center" fxLayoutGap="8px"> + <img class="google-icon" src="assets/img/icons/g-logo.png" i18n-alt alt="Google logo" /> + <span i18n>This profile is linked to a Google account.</span> + </div> + <button class="unlink" type="button" mat-raised-button (click)="unlinkGoogleAccount()"> + <span class="warn" i18n>Unlink Google Account</span> </button> </div> </div> diff --git a/src/main/webapp/site/src/app/student/account/edit-profile/edit-profile.component.scss b/src/main/webapp/site/src/app/student/account/edit-profile/edit-profile.component.scss index 36ff60ef97..a1e699e818 100644 --- a/src/main/webapp/site/src/app/student/account/edit-profile/edit-profile.component.scss +++ b/src/main/webapp/site/src/app/student/account/edit-profile/edit-profile.component.scss @@ -3,10 +3,19 @@ '~style/abstracts/functions', '~style/abstracts/mixins'; -form { +.inputs { max-width: breakpoint('sm.min'); +} + +.actions { + margin-top: 8px; +} + +.google-icon { + height: 1.8em; + width: auto; +} - @media (max-width: breakpoint('sm.max')) { - margin: 0 auto; - } +.unlink { + margin: 8px 0; } diff --git a/src/main/webapp/site/src/app/student/account/edit-profile/edit-profile.component.spec.ts b/src/main/webapp/site/src/app/student/account/edit-profile/edit-profile.component.spec.ts index c338a4a343..5c425f8766 100644 --- a/src/main/webapp/site/src/app/student/account/edit-profile/edit-profile.component.spec.ts +++ b/src/main/webapp/site/src/app/student/account/edit-profile/edit-profile.component.spec.ts @@ -13,6 +13,7 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { By } from '@angular/platform-browser'; import { Student } from '../../../domain/student'; import { configureTestSuite } from 'ng-bullet'; +import { MatDialogModule } from '@angular/material/dialog'; export class MockUserService { user: User; @@ -76,6 +77,7 @@ describe('EditProfileComponent', () => { imports: [ BrowserAnimationsModule, ReactiveFormsModule, + MatDialogModule, MatInputModule, MatSelectModule, MatSnackBarModule diff --git a/src/main/webapp/site/src/app/student/account/edit-profile/edit-profile.component.ts b/src/main/webapp/site/src/app/student/account/edit-profile/edit-profile.component.ts index ea256cd802..fe6d7b3452 100644 --- a/src/main/webapp/site/src/app/student/account/edit-profile/edit-profile.component.ts +++ b/src/main/webapp/site/src/app/student/account/edit-profile/edit-profile.component.ts @@ -1,22 +1,26 @@ -import { Component, OnInit } from '@angular/core'; +import { Component } from '@angular/core'; import { FormGroup, FormControl, Validators, FormBuilder } from '@angular/forms'; import { finalize } from 'rxjs/operators'; import { MatSnackBar } from '@angular/material/snack-bar'; import { Student } from '../../../domain/student'; import { UserService } from '../../../services/user.service'; import { StudentService } from '../../student.service'; +import { Subscription } from 'rxjs'; +import { MatDialog } from '@angular/material/dialog'; +import { UnlinkGoogleAccountConfirmComponent } from '../../../modules/shared/unlink-google-account-confirm/unlink-google-account-confirm.component'; @Component({ selector: 'app-edit-profile', templateUrl: './edit-profile.component.html', styleUrls: ['./edit-profile.component.scss'] }) -export class EditProfileComponent implements OnInit { +export class EditProfileComponent { user: Student; languages: object[]; changed: boolean = false; isSaving: boolean = false; - + isGoogleUser: boolean = false; + userSubscription: Subscription; editProfileFormGroup: FormGroup = this.fb.group({ firstName: new FormControl({ value: '', disabled: true }, [Validators.required]), lastName: new FormControl({ value: '', disabled: true }, [Validators.required]), @@ -28,6 +32,7 @@ export class EditProfileComponent implements OnInit { private fb: FormBuilder, private studentService: StudentService, private userService: UserService, + public dialog: MatDialog, public snackBar: MatSnackBar ) { this.user = <Student>this.getUser().getValue(); @@ -38,10 +43,6 @@ export class EditProfileComponent implements OnInit { this.userService.getLanguages().subscribe((response) => { this.languages = <object[]>response; }); - - this.editProfileFormGroup.valueChanges.subscribe(() => { - this.changed = true; - }); } getUser() { @@ -52,7 +53,18 @@ export class EditProfileComponent implements OnInit { this.editProfileFormGroup.controls[name].setValue(value); } - ngOnInit() {} + ngOnInit() { + this.editProfileFormGroup.valueChanges.subscribe(() => { + this.changed = true; + }); + this.userSubscription = this.userService.getUser().subscribe((user) => { + this.isGoogleUser = user.isGoogleUser; + }); + } + + ngOnDestroy() { + this.userSubscription.unsubscribe(); + } saveChanges() { this.isSaving = true; @@ -84,4 +96,10 @@ export class EditProfileComponent implements OnInit { } this.isSaving = false; } + + unlinkGoogleAccount() { + this.dialog.open(UnlinkGoogleAccountConfirmComponent, { + panelClass: 'mat-dialog--sm' + }); + } } diff --git a/src/main/webapp/site/src/app/teacher-hybrid-angular.module.ts b/src/main/webapp/site/src/app/teacher-hybrid-angular.module.ts index e9a9ff545a..3f6c1c6e27 100644 --- a/src/main/webapp/site/src/app/teacher-hybrid-angular.module.ts +++ b/src/main/webapp/site/src/app/teacher-hybrid-angular.module.ts @@ -51,6 +51,9 @@ import { MultipleChoiceAuthoring } from '../../../wise5/components/multipleChoic import { ConceptMapAuthoring } from '../../../wise5/components/conceptMap/concept-map-authoring/concept-map-authoring.component'; import { DrawAuthoring } from '../../../wise5/components/draw/draw-authoring/draw-authoring.component'; import { MatchAuthoring } from '../../../wise5/components/match/match-authoring/match-authoring.component'; +import { LabelAuthoring } from '../../../wise5/components/label/label-authoring/label-authoring.component'; +import { TableAuthoring } from '../../../wise5/components/table/table-authoring/table-authoring.component'; +import { DiscussionAuthoring } from '../../../wise5/components/discussion/discussion-authoring/discussion-authoring.component'; @NgModule({ declarations: [ @@ -64,6 +67,7 @@ import { MatchAuthoring } from '../../../wise5/components/match/match-authoring/ ComponentSelectComponent, ConceptMapAuthoring, DrawAuthoring, + DiscussionAuthoring, EditComponentRubricComponent, EditComponentJsonComponent, EditComponentMaxScoreComponent, @@ -72,6 +76,7 @@ import { MatchAuthoring } from '../../../wise5/components/match/match-authoring/ EditHTMLAdvancedComponent, EditOutsideUrlAdvancedComponent, HtmlAuthoring, + LabelAuthoring, ManageStudentsComponent, MatchAuthoring, MilestonesComponent, @@ -85,6 +90,7 @@ import { MatchAuthoring } from '../../../wise5/components/match/match-authoring/ RubricAuthoringComponent, StatusIconComponent, StepInfoComponent, + TableAuthoring, WorkgroupInfoComponent, WorkgroupNodeScoreComponent, WorkgroupSelectAutocompleteComponent, diff --git a/src/main/webapp/site/src/app/teacher/account/edit-profile/edit-profile.component.html b/src/main/webapp/site/src/app/teacher/account/edit-profile/edit-profile.component.html index b4f5bd0f54..159c38c1b1 100644 --- a/src/main/webapp/site/src/app/teacher/account/edit-profile/edit-profile.component.html +++ b/src/main/webapp/site/src/app/teacher/account/edit-profile/edit-profile.component.html @@ -105,19 +105,36 @@ </mat-option> </mat-select> <mat-hint i18n>Help us translate WISE! Visit <a href="https://crowdin.com/project/wise" target="_blank">https://crowdin.com/project/wise</a>.</mat-hint> - <mat-error *ngIf="editProfileFormGroup.controls['language'].hasError('required')" i18n>Language required</mat-error> + <mat-error *ngIf="editProfileFormGroup.controls['language'].hasError('required')" i18n> + Language required + </mat-error> </mat-form-field> </p> </div> - <div> + <div class="actions" + fxLayout="column" + fxLayout.gt-sm="row" + fxLayoutAlign="center start" + fxLayoutAlign.gt-sm="start center" + fxLayoutGap="24px"> <button mat-raised-button color="primary" type="submit" - [disabled]="!editProfileFormGroup.valid || !changed || isSaving" - fxFlex - fxFlex.gt-xs="0 0 auto"> + [disabled]="!editProfileFormGroup.valid || !changed || isSaving"> <mat-progress-bar mode="indeterminate" *ngIf="isSaving"></mat-progress-bar> <ng-container i18n>Save Changes</ng-container> </button> + <div *ngIf="isGoogleUser" + fxLayout="row wrap" + fxLayoutAlign="start center" + fxLayoutGap="8px"> + <div fxLayoutAlign="start center" fxLayoutGap="8px"> + <img class="google-icon" src="assets/img/icons/g-logo.png" i18n-alt alt="Google logo" /> + <span i18n>This profile is linked to a Google account.</span> + </div> + <button class="unlink" type="button" mat-raised-button (click)="unlinkGoogleAccount()"> + <span class="warn" i18n>Unlink Google Account</span> + </button> + </div> </div> </form> diff --git a/src/main/webapp/site/src/app/teacher/account/edit-profile/edit-profile.component.scss b/src/main/webapp/site/src/app/teacher/account/edit-profile/edit-profile.component.scss index dbd834f0b1..1888291374 100644 --- a/src/main/webapp/site/src/app/teacher/account/edit-profile/edit-profile.component.scss +++ b/src/main/webapp/site/src/app/teacher/account/edit-profile/edit-profile.component.scss @@ -21,3 +21,16 @@ } } } + +.actions { + margin-top: 8px; +} + +.google-icon { + height: 1.8em; + width: auto; +} + +.unlink { + margin: 8px 0; +} diff --git a/src/main/webapp/site/src/app/teacher/account/edit-profile/edit-profile.component.spec.ts b/src/main/webapp/site/src/app/teacher/account/edit-profile/edit-profile.component.spec.ts index e8fe132d86..05b8ad23a2 100644 --- a/src/main/webapp/site/src/app/teacher/account/edit-profile/edit-profile.component.spec.ts +++ b/src/main/webapp/site/src/app/teacher/account/edit-profile/edit-profile.component.spec.ts @@ -13,6 +13,7 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { By } from '@angular/platform-browser'; import { User } from '../../../domain/user'; import { configureTestSuite } from 'ng-bullet'; +import { MatDialogModule } from '@angular/material/dialog'; export class MockUserService { user: User; @@ -89,6 +90,7 @@ describe('EditProfileComponent', () => { imports: [ BrowserAnimationsModule, ReactiveFormsModule, + MatDialogModule, MatInputModule, MatSelectModule, MatSnackBarModule diff --git a/src/main/webapp/site/src/app/teacher/account/edit-profile/edit-profile.component.ts b/src/main/webapp/site/src/app/teacher/account/edit-profile/edit-profile.component.ts index 6ed9c50c25..01c612ea48 100644 --- a/src/main/webapp/site/src/app/teacher/account/edit-profile/edit-profile.component.ts +++ b/src/main/webapp/site/src/app/teacher/account/edit-profile/edit-profile.component.ts @@ -1,17 +1,20 @@ -import { Component, OnInit } from '@angular/core'; +import { Component } from '@angular/core'; import { FormControl, FormGroup, Validators, FormBuilder } from '@angular/forms'; import { finalize } from 'rxjs/operators'; import { MatSnackBar } from '@angular/material/snack-bar'; import { UserService } from '../../../services/user.service'; import { Teacher } from '../../../domain/teacher'; import { TeacherService } from '../../teacher.service'; +import { MatDialog } from '@angular/material/dialog'; +import { UnlinkGoogleAccountConfirmComponent } from '../../../modules/shared/unlink-google-account-confirm/unlink-google-account-confirm.component'; +import { Subscription } from 'rxjs'; @Component({ selector: 'app-edit-profile', templateUrl: './edit-profile.component.html', styleUrls: ['./edit-profile.component.scss'] }) -export class EditProfileComponent implements OnInit { +export class EditProfileComponent { user: Teacher; schoolLevels: any[] = [ { id: 'ELEMENTARY_SCHOOL', label: $localize`Elementary School` }, @@ -23,6 +26,8 @@ export class EditProfileComponent implements OnInit { languages: object[]; changed: boolean = false; isSaving: boolean = false; + isGoogleUser: boolean = false; + userSubscription: Subscription; editProfileFormGroup: FormGroup = this.fb.group({ firstName: new FormControl({ value: '', disabled: true }, [Validators.required]), @@ -41,6 +46,7 @@ export class EditProfileComponent implements OnInit { private fb: FormBuilder, private teacherService: TeacherService, private userService: UserService, + public dialog: MatDialog, public snackBar: MatSnackBar ) { this.user = <Teacher>this.getUser().getValue(); @@ -57,10 +63,6 @@ export class EditProfileComponent implements OnInit { this.userService.getLanguages().subscribe((response) => { this.languages = <object[]>response; }); - - this.editProfileFormGroup.valueChanges.subscribe(() => { - this.changed = true; - }); } getUser() { @@ -71,7 +73,19 @@ export class EditProfileComponent implements OnInit { this.editProfileFormGroup.controls[name].setValue(value); } - ngOnInit() {} + ngOnInit() { + this.editProfileFormGroup.valueChanges.subscribe(() => { + this.changed = true; + }); + + this.userSubscription = this.userService.getUser().subscribe((user) => { + this.isGoogleUser = user.isGoogleUser; + }); + } + + ngOnDestroy() { + this.userSubscription.unsubscribe(); + } saveChanges() { this.isSaving = true; @@ -128,4 +142,10 @@ export class EditProfileComponent implements OnInit { this.snackBar.open($localize`An error occurred. Please try again.`); } } + + unlinkGoogleAccount() { + this.dialog.open(UnlinkGoogleAccountConfirmComponent, { + panelClass: 'mat-dialog--sm' + }); + } } diff --git a/src/main/webapp/site/src/app/teacher/edit-run-warning-dialog/edit-run-warning-dialog.component.html b/src/main/webapp/site/src/app/teacher/edit-run-warning-dialog/edit-run-warning-dialog.component.html index 1f9c2f19f8..c196fa4e2f 100644 --- a/src/main/webapp/site/src/app/teacher/edit-run-warning-dialog/edit-run-warning-dialog.component.html +++ b/src/main/webapp/site/src/app/teacher/edit-run-warning-dialog/edit-run-warning-dialog.component.html @@ -1,7 +1,7 @@ -<h2 mat-dialog-title fxLayoutAlign="start center" class="warn"> +<h2 mat-dialog-title fxLayoutAlign="start center"> <span i18n>Edit Classroom Unit</span> <span fxFlex></span> - <mat-icon color="warn">warning</mat-icon> + <mat-icon color="warn" i18n-aria-label aria-label="Warning">warning</mat-icon> </h2> <mat-dialog-content> <div class="info-block"> diff --git a/src/main/webapp/site/src/messages.xlf b/src/main/webapp/site/src/messages.xlf index 2419779b02..01901da39b 100644 --- a/src/main/webapp/site/src/messages.xlf +++ b/src/main/webapp/site/src/messages.xlf @@ -236,29 +236,138 @@ <context context-type="linenumber">10</context> </context-group> </trans-unit> - <trans-unit datatype="html" id="1ae3dcf96b494406b6fd6fbd2f15f7259553bf4d"> - <source>Current Password</source> + <trans-unit datatype="html" id="692e665f8177ee9150169dca0ff15418616cb3cb"> + <source>Google logo</source> + <context-group purpose="location"> + <context context-type="sourcefile">app/modules/shared/unlink-google-account-success/unlink-google-account-success.component.html</context> + <context context-type="linenumber">2</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">app/modules/shared/unlink-google-account-password/unlink-google-account-password.component.html</context> + <context context-type="linenumber">2</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">app/modules/shared/unlink-google-account-confirm/unlink-google-account-confirm.component.html</context> + <context context-type="linenumber">2</context> + </context-group> <context-group purpose="location"> <context context-type="sourcefile">app/modules/shared/edit-password/edit-password.component.html</context> - <context context-type="linenumber">8</context> + <context context-type="linenumber">61</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">app/register/register-teacher/register-teacher.component.html</context> + <context context-type="linenumber">28</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">app/register/register-teacher-complete/register-teacher-complete.component.html</context> + <context context-type="linenumber">14</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">app/register/register-student-complete/register-student-complete.component.html</context> + <context context-type="linenumber">14</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">app/register/register-student/register-student.component.html</context> + <context context-type="linenumber">39</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">app/register/register-google-user-already-exists/register-google-user-already-exists.component.html</context> + <context context-type="linenumber">10</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">app/student/team-sign-in-dialog/team-sign-in-dialog.component.html</context> + <context context-type="linenumber">59</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">app/student/account/edit-profile/edit-profile.component.html</context> + <context context-type="linenumber">64</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">app/teacher/account/edit-profile/edit-profile.component.html</context> + <context context-type="linenumber">132</context> </context-group> </trans-unit> - <trans-unit datatype="html" id="9b79b69a1ba8ba29e68a45acd0829da17da704ce"> - <source>Current Password required</source> + <trans-unit datatype="html" id="47c3500e23cf10b4e86b3c495687d5045fd2305c"> + <source>Unlink Google Account</source> + <context-group purpose="location"> + <context context-type="sourcefile">app/modules/shared/unlink-google-account-success/unlink-google-account-success.component.html</context> + <context context-type="linenumber">3</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">app/modules/shared/unlink-google-account-password/unlink-google-account-password.component.html</context> + <context context-type="linenumber">3</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">app/modules/shared/unlink-google-account-confirm/unlink-google-account-confirm.component.html</context> + <context context-type="linenumber">3</context> + </context-group> <context-group purpose="location"> <context context-type="sourcefile">app/modules/shared/edit-password/edit-password.component.html</context> - <context context-type="linenumber">15</context> + <context context-type="linenumber">70</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">app/student/account/edit-profile/edit-profile.component.html</context> + <context context-type="linenumber">68</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">app/teacher/account/edit-profile/edit-profile.component.html</context> + <context context-type="linenumber">136</context> </context-group> </trans-unit> - <trans-unit datatype="html" id="0315d84608647a2d4a2b2e7e509d10ad0ec78536"> - <source>Current Password is incorrect</source> + <trans-unit datatype="html" id="09031a48e6666b30516536554bd71ec9073bc906"> + <source>Success! You have unlinked your Google account from WISE. To sign in to WISE in the future, please use your username and the password you just created.</source> <context-group purpose="location"> - <context context-type="sourcefile">app/modules/shared/edit-password/edit-password.component.html</context> - <context context-type="linenumber">16</context> + <context context-type="sourcefile">app/modules/shared/unlink-google-account-success/unlink-google-account-success.component.html</context> + <context context-type="linenumber">7</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="b3062cd2df40541e781d0e69c02c670cf3c38fa9"> + <source>Your username is: <x ctype="x-span" equiv-text="<span>" id="START_TAG_SPAN"/><x equiv-text="{{ username }}" id="INTERPOLATION"/><x ctype="x-span" equiv-text="</span>" id="CLOSE_TAG_SPAN"/>.</source> + <context-group purpose="location"> + <context context-type="sourcefile">app/modules/shared/unlink-google-account-success/unlink-google-account-success.component.html</context> + <context context-type="linenumber">8</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="8dd413cee2228118c536f503709329a4d1a395e2"> + <source>Done</source> + <context-group purpose="location"> + <context context-type="sourcefile">app/modules/shared/unlink-google-account-success/unlink-google-account-success.component.html</context> + <context context-type="linenumber">12</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">app/teacher/list-classroom-courses-dialog/list-classroom-courses-dialog.component.html</context> + <context context-type="linenumber">75</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">app/teacher/create-run-dialog/create-run-dialog.component.html</context> + <context context-type="linenumber">85</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">app/modules/library/share-project-dialog/share-project-dialog.component.html</context> + <context context-type="linenumber">70</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">app/teacher/share-run-dialog/share-run-dialog.component.html</context> + <context context-type="linenumber">102</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">app/teacher/run-settings-dialog/run-settings-dialog.component.html</context> + <context context-type="linenumber">76</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="31874c167522896bc9ac326873703ffde25ede7f"> + <source>Create a WISE password:</source> + <context-group purpose="location"> + <context context-type="sourcefile">app/modules/shared/unlink-google-account-password/unlink-google-account-password.component.html</context> + <context context-type="linenumber">7</context> </context-group> </trans-unit> <trans-unit datatype="html" id="c7014c6360e94b236286b869c3fe0ea9911c0387"> <source>New Password</source> + <context-group purpose="location"> + <context context-type="sourcefile">app/modules/shared/unlink-google-account-password/unlink-google-account-password.component.html</context> + <context context-type="linenumber">9</context> + </context-group> <context-group purpose="location"> <context context-type="sourcefile">app/modules/shared/edit-password/edit-password.component.html</context> <context context-type="linenumber">22</context> @@ -266,6 +375,10 @@ </trans-unit> <trans-unit datatype="html" id="ae26b4e37fa94304d1fa1b0c46b10ad979bcecfa"> <source>New Password required</source> + <context-group purpose="location"> + <context context-type="sourcefile">app/modules/shared/unlink-google-account-password/unlink-google-account-password.component.html</context> + <context context-type="linenumber">16</context> + </context-group> <context-group purpose="location"> <context context-type="sourcefile">app/modules/shared/edit-password/edit-password.component.html</context> <context context-type="linenumber">29</context> @@ -273,6 +386,10 @@ </trans-unit> <trans-unit datatype="html" id="b81fbd1b5d0723eba41870071d02065674bdd565"> <source>Confirm New Password</source> + <context-group purpose="location"> + <context context-type="sourcefile">app/modules/shared/unlink-google-account-password/unlink-google-account-password.component.html</context> + <context context-type="linenumber">19</context> + </context-group> <context-group purpose="location"> <context context-type="sourcefile">app/modules/shared/edit-password/edit-password.component.html</context> <context context-type="linenumber">34</context> @@ -280,6 +397,10 @@ </trans-unit> <trans-unit datatype="html" id="4b4b336ca43da85eee55391d95fbc03d921353b5"> <source>Confirm Password required</source> + <context-group purpose="location"> + <context context-type="sourcefile">app/modules/shared/unlink-google-account-password/unlink-google-account-password.component.html</context> + <context context-type="linenumber">26</context> + </context-group> <context-group purpose="location"> <context context-type="sourcefile">app/modules/shared/edit-password/edit-password.component.html</context> <context context-type="linenumber">41</context> @@ -303,11 +424,170 @@ </trans-unit> <trans-unit datatype="html" id="0a9dcaac5aadd48fe716eaff77ab9b21a8bef3af"> <source>Passwords do not match</source> + <context-group purpose="location"> + <context context-type="sourcefile">app/modules/shared/unlink-google-account-password/unlink-google-account-password.component.html</context> + <context context-type="linenumber">27</context> + </context-group> <context-group purpose="location"> <context context-type="sourcefile">app/modules/shared/edit-password/edit-password.component.html</context> <context context-type="linenumber">42</context> </context-group> </trans-unit> + <trans-unit datatype="html" id="d7b35c384aecd25a516200d6921836374613dfe7"> + <source>Cancel</source> + <context-group purpose="location"> + <context context-type="sourcefile">app/modules/shared/unlink-google-account-password/unlink-google-account-password.component.html</context> + <context context-type="linenumber">31</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">app/modules/shared/unlink-google-account-confirm/unlink-google-account-confirm.component.html</context> + <context context-type="linenumber">14</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">app/modules/library/copy-project-dialog/copy-project-dialog.component.html</context> + <context context-type="linenumber">11</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">app/teacher/list-classroom-courses-dialog/list-classroom-courses-dialog.component.html</context> + <context context-type="linenumber">47</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">app/teacher/create-run-dialog/create-run-dialog.component.html</context> + <context context-type="linenumber">63</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">app/teacher/use-with-class-warning-dialog/use-with-class-warning-dialog.component.html</context> + <context context-type="linenumber">15</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">app/teacher/edit-run-warning-dialog/edit-run-warning-dialog.component.html</context> + <context context-type="linenumber">20</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">app/student/add-project-dialog/add-project-dialog.component.html</context> + <context context-type="linenumber">23</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">app/student/team-sign-in-dialog/team-sign-in-dialog.component.html</context> + <context context-type="linenumber">68</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">app/teacher/share-run-dialog/share-run-dialog.component.html</context> + <context context-type="linenumber">100</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">app/authoring-tool/import-step/choose-import-step/choose-import-step.component.html</context> + <context context-type="linenumber">62</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">app/authoring-tool/import-step/choose-import-step-location/choose-import-step-location.component.html</context> + <context context-type="linenumber">40</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">app/authoring-tool/add-component/choose-new-component/choose-new-component.component.html</context> + <context context-type="linenumber">23</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">app/authoring-tool/add-component/choose-new-component-location/choose-new-component-location.component.html</context> + <context context-type="linenumber">41</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="71c77bb8cecdf11ec3eead24dd1ba506573fa9cd"> + <source>Submit</source> + <context-group purpose="location"> + <context context-type="sourcefile">app/modules/shared/unlink-google-account-password/unlink-google-account-password.component.html</context> + <context context-type="linenumber">37</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">app/contact/contact-form/contact-form.component.html</context> + <context context-type="linenumber">112</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">app/forgot/student/forgot-student-password/forgot-student-password.component.html</context> + <context context-type="linenumber">25</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">app/forgot/teacher/forgot-teacher-username/forgot-teacher-username.component.html</context> + <context context-type="linenumber">25</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">app/forgot/teacher/forgot-teacher-password/forgot-teacher-password.component.html</context> + <context context-type="linenumber">25</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">app/forgot/student/forgot-student-password-security/forgot-student-password-security.component.html</context> + <context context-type="linenumber">28</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">app/forgot/student/forgot-student-password-change/forgot-student-password-change.component.html</context> + <context context-type="linenumber">41</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">app/forgot/teacher/forgot-teacher-password-change/forgot-teacher-password-change.component.html</context> + <context context-type="linenumber">39</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">app/forgot/teacher/forgot-teacher-password-verify/forgot-teacher-password-verify.component.html</context> + <context context-type="linenumber">26</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="a8059e31694578c1b0344a76a345357dd60e8f01"> + <source>Warning</source> + <context-group purpose="location"> + <context context-type="sourcefile">app/modules/shared/unlink-google-account-confirm/unlink-google-account-confirm.component.html</context> + <context context-type="linenumber">5</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">app/teacher/edit-run-warning-dialog/edit-run-warning-dialog.component.html</context> + <context context-type="linenumber">4</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">app/help/teacher-faq/teacher-faq.component.html</context> + <context context-type="linenumber">85</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="5d9fa1b5819208cfbc678e570f4bd34144bcfe81"> + <source>To remove the link to your Google account, you will be asked to create a WISE password. In the future, you'll sign in to WISE using your username and password.</source> + <context-group purpose="location"> + <context context-type="sourcefile">app/modules/shared/unlink-google-account-confirm/unlink-google-account-confirm.component.html</context> + <context context-type="linenumber">9</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="f021455ae88703603db9bb34ea44ab9170f37920"> + <source>You will no longer be able to sign in to WISE using Google. Would you like to continue?</source> + <context-group purpose="location"> + <context context-type="sourcefile">app/modules/shared/unlink-google-account-confirm/unlink-google-account-confirm.component.html</context> + <context context-type="linenumber">10</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="ac10a3d9b59575640797c1a8e6aea642cf5d5e77"> + <source>Continue</source> + <context-group purpose="location"> + <context context-type="sourcefile">app/modules/shared/unlink-google-account-confirm/unlink-google-account-confirm.component.html</context> + <context context-type="linenumber">15</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="1ae3dcf96b494406b6fd6fbd2f15f7259553bf4d"> + <source>Current Password</source> + <context-group purpose="location"> + <context context-type="sourcefile">app/modules/shared/edit-password/edit-password.component.html</context> + <context context-type="linenumber">8</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="9b79b69a1ba8ba29e68a45acd0829da17da704ce"> + <source>Current Password required</source> + <context-group purpose="location"> + <context context-type="sourcefile">app/modules/shared/edit-password/edit-password.component.html</context> + <context context-type="linenumber">15</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="0315d84608647a2d4a2b2e7e509d10ad0ec78536"> + <source>Current Password is incorrect</source> + <context-group purpose="location"> + <context context-type="sourcefile">app/modules/shared/edit-password/edit-password.component.html</context> + <context context-type="linenumber">16</context> + </context-group> + </trans-unit> <trans-unit datatype="html" id="5edf054f14aa57bc25a28cb7db01956fee53dac8"> <source>Change Password</source> <context-group purpose="location"> @@ -323,8 +603,8 @@ <context context-type="linenumber">7</context> </context-group> </trans-unit> - <trans-unit datatype="html" id="1622b67543d6f79dc5e9730e121f7e21b3d034b1"> - <source>This account was created using Google and doesn't use a WISE password. If you would like to unlink your Google account, please <x ctype="x-a" equiv-text="<a>" id="START_LINK"/>contact us<x ctype="x-a" equiv-text="</a>" id="CLOSE_LINK"/>.</source> + <trans-unit datatype="html" id="8454739bcb4cf4debfd509a35e466a828ea11363"> + <source><x ctype="image" equiv-text="<img/>" id="TAG_IMG"/><x ctype="x-span" equiv-text="<span>" id="START_TAG_SPAN"/>This account was created using Google and doesn't use a WISE password.<x ctype="x-span" equiv-text="</span>" id="CLOSE_TAG_SPAN"/></source> <context-group purpose="location"> <context context-type="sourcefile">app/modules/shared/edit-password/edit-password.component.html</context> <context context-type="linenumber">60</context> @@ -1068,57 +1348,6 @@ <context context-type="linenumber">7</context> </context-group> </trans-unit> - <trans-unit datatype="html" id="d7b35c384aecd25a516200d6921836374613dfe7"> - <source>Cancel</source> - <context-group purpose="location"> - <context context-type="sourcefile">app/modules/library/copy-project-dialog/copy-project-dialog.component.html</context> - <context context-type="linenumber">11</context> - </context-group> - <context-group purpose="location"> - <context context-type="sourcefile">app/teacher/list-classroom-courses-dialog/list-classroom-courses-dialog.component.html</context> - <context context-type="linenumber">47</context> - </context-group> - <context-group purpose="location"> - <context context-type="sourcefile">app/teacher/create-run-dialog/create-run-dialog.component.html</context> - <context context-type="linenumber">63</context> - </context-group> - <context-group purpose="location"> - <context context-type="sourcefile">app/teacher/use-with-class-warning-dialog/use-with-class-warning-dialog.component.html</context> - <context context-type="linenumber">15</context> - </context-group> - <context-group purpose="location"> - <context context-type="sourcefile">app/teacher/edit-run-warning-dialog/edit-run-warning-dialog.component.html</context> - <context context-type="linenumber">20</context> - </context-group> - <context-group purpose="location"> - <context context-type="sourcefile">app/student/add-project-dialog/add-project-dialog.component.html</context> - <context context-type="linenumber">23</context> - </context-group> - <context-group purpose="location"> - <context context-type="sourcefile">app/student/team-sign-in-dialog/team-sign-in-dialog.component.html</context> - <context context-type="linenumber">68</context> - </context-group> - <context-group purpose="location"> - <context context-type="sourcefile">app/teacher/share-run-dialog/share-run-dialog.component.html</context> - <context context-type="linenumber">100</context> - </context-group> - <context-group purpose="location"> - <context context-type="sourcefile">app/authoring-tool/import-step/choose-import-step/choose-import-step.component.html</context> - <context context-type="linenumber">62</context> - </context-group> - <context-group purpose="location"> - <context context-type="sourcefile">app/authoring-tool/import-step/choose-import-step-location/choose-import-step-location.component.html</context> - <context context-type="linenumber">40</context> - </context-group> - <context-group purpose="location"> - <context context-type="sourcefile">app/authoring-tool/add-component/choose-new-component/choose-new-component.component.html</context> - <context context-type="linenumber">23</context> - </context-group> - <context-group purpose="location"> - <context context-type="sourcefile">app/authoring-tool/add-component/choose-new-component-location/choose-new-component-location.component.html</context> - <context context-type="linenumber">41</context> - </context-group> - </trans-unit> <trans-unit datatype="html" id="1979da7460819153e11d2078244645d94291b69c"> <source>Copy</source> <context-group purpose="location"> @@ -1256,29 +1485,6 @@ <context context-type="linenumber">69</context> </context-group> </trans-unit> - <trans-unit datatype="html" id="8dd413cee2228118c536f503709329a4d1a395e2"> - <source>Done</source> - <context-group purpose="location"> - <context context-type="sourcefile">app/teacher/list-classroom-courses-dialog/list-classroom-courses-dialog.component.html</context> - <context context-type="linenumber">75</context> - </context-group> - <context-group purpose="location"> - <context context-type="sourcefile">app/teacher/create-run-dialog/create-run-dialog.component.html</context> - <context context-type="linenumber">85</context> - </context-group> - <context-group purpose="location"> - <context context-type="sourcefile">app/modules/library/share-project-dialog/share-project-dialog.component.html</context> - <context context-type="linenumber">70</context> - </context-group> - <context-group purpose="location"> - <context context-type="sourcefile">app/teacher/share-run-dialog/share-run-dialog.component.html</context> - <context context-type="linenumber">102</context> - </context-group> - <context-group purpose="location"> - <context context-type="sourcefile">app/teacher/run-settings-dialog/run-settings-dialog.component.html</context> - <context context-type="linenumber">76</context> - </context-group> - </trans-unit> <trans-unit datatype="html" id="053c99466a9288207400b49f2ca907192b5bd44d"> <source>Use with Class</source> <context-group purpose="location"> @@ -2294,41 +2500,6 @@ <context context-type="linenumber">104</context> </context-group> </trans-unit> - <trans-unit datatype="html" id="71c77bb8cecdf11ec3eead24dd1ba506573fa9cd"> - <source>Submit</source> - <context-group purpose="location"> - <context context-type="sourcefile">app/contact/contact-form/contact-form.component.html</context> - <context context-type="linenumber">112</context> - </context-group> - <context-group purpose="location"> - <context context-type="sourcefile">app/forgot/student/forgot-student-password/forgot-student-password.component.html</context> - <context context-type="linenumber">25</context> - </context-group> - <context-group purpose="location"> - <context context-type="sourcefile">app/forgot/teacher/forgot-teacher-username/forgot-teacher-username.component.html</context> - <context context-type="linenumber">25</context> - </context-group> - <context-group purpose="location"> - <context context-type="sourcefile">app/forgot/teacher/forgot-teacher-password/forgot-teacher-password.component.html</context> - <context context-type="linenumber">25</context> - </context-group> - <context-group purpose="location"> - <context context-type="sourcefile">app/forgot/student/forgot-student-password-security/forgot-student-password-security.component.html</context> - <context context-type="linenumber">28</context> - </context-group> - <context-group purpose="location"> - <context context-type="sourcefile">app/forgot/student/forgot-student-password-change/forgot-student-password-change.component.html</context> - <context context-type="linenumber">41</context> - </context-group> - <context-group purpose="location"> - <context context-type="sourcefile">app/forgot/teacher/forgot-teacher-password-change/forgot-teacher-password-change.component.html</context> - <context context-type="linenumber">39</context> - </context-group> - <context-group purpose="location"> - <context context-type="sourcefile">app/forgot/teacher/forgot-teacher-password-verify/forgot-teacher-password-verify.component.html</context> - <context context-type="linenumber">26</context> - </context-group> - </trans-unit> <trans-unit datatype="html" id="abc0c762d328168e523abf84749240e43593f8df"> <source>WISE Features</source> <context-group purpose="location"> @@ -3850,13 +4021,6 @@ <context context-type="linenumber">84</context> </context-group> </trans-unit> - <trans-unit datatype="html" id="a8059e31694578c1b0344a76a345357dd60e8f01"> - <source>Warning</source> - <context-group purpose="location"> - <context context-type="sourcefile">app/help/teacher-faq/teacher-faq.component.html</context> - <context context-type="linenumber">85</context> - </context-group> - </trans-unit> <trans-unit datatype="html" id="4ad84d4f3bbc9a37b27cd1ff908aea5fa83dccf3"> <source>If you move a student to a different period, they will lose all of their work.</source> <context-group purpose="location"> @@ -4563,50 +4727,23 @@ <context context-type="sourcefile">app/register/register-teacher-form/register-teacher-form.component.html</context> <context context-type="linenumber">7</context> </context-group> - </trans-unit> - <trans-unit datatype="html" id="28629af803ab8b5f6e995b17d486e6a1527e2a96"> - <source>Sign Up</source> - <context-group purpose="location"> - <context context-type="sourcefile">app/register/register-teacher/register-teacher.component.html</context> - <context context-type="linenumber">18</context> - </context-group> - <context-group purpose="location"> - <context context-type="sourcefile">app/register/register-student/register-student.component.html</context> - <context context-type="linenumber">29</context> - </context-group> - </trans-unit> - <trans-unit datatype="html" id="223846e3b440a5bf278a46411d71586ac79cc6ec"> - <source>- or -</source> - <context-group purpose="location"> - <context context-type="sourcefile">app/register/register-teacher/register-teacher.component.html</context> - <context context-type="linenumber">22</context> - </context-group> - </trans-unit> - <trans-unit datatype="html" id="692e665f8177ee9150169dca0ff15418616cb3cb"> - <source>Google logo</source> - <context-group purpose="location"> - <context context-type="sourcefile">app/register/register-teacher/register-teacher.component.html</context> - <context context-type="linenumber">28</context> - </context-group> - <context-group purpose="location"> - <context context-type="sourcefile">app/register/register-teacher-complete/register-teacher-complete.component.html</context> - <context context-type="linenumber">14</context> - </context-group> - <context-group purpose="location"> - <context context-type="sourcefile">app/register/register-student-complete/register-student-complete.component.html</context> - <context context-type="linenumber">14</context> - </context-group> + </trans-unit> + <trans-unit datatype="html" id="28629af803ab8b5f6e995b17d486e6a1527e2a96"> + <source>Sign Up</source> <context-group purpose="location"> - <context context-type="sourcefile">app/register/register-student/register-student.component.html</context> - <context context-type="linenumber">39</context> + <context context-type="sourcefile">app/register/register-teacher/register-teacher.component.html</context> + <context context-type="linenumber">18</context> </context-group> <context-group purpose="location"> - <context context-type="sourcefile">app/register/register-google-user-already-exists/register-google-user-already-exists.component.html</context> - <context context-type="linenumber">10</context> + <context context-type="sourcefile">app/register/register-student/register-student.component.html</context> + <context context-type="linenumber">29</context> </context-group> + </trans-unit> + <trans-unit datatype="html" id="223846e3b440a5bf278a46411d71586ac79cc6ec"> + <source>- or -</source> <context-group purpose="location"> - <context context-type="sourcefile">app/student/team-sign-in-dialog/team-sign-in-dialog.component.html</context> - <context context-type="linenumber">59</context> + <context context-type="sourcefile">app/register/register-teacher/register-teacher.component.html</context> + <context context-type="linenumber">22</context> </context-group> </trans-unit> <trans-unit datatype="html" id="49ddce3277c9c81865f8d2278255426cdc9e94f2"> @@ -5290,20 +5427,27 @@ <context context-type="sourcefile">app/student/account/edit-profile/edit-profile.component.html</context> <context context-type="linenumber">42</context> </context-group> + </trans-unit> + <trans-unit datatype="html" id="afa960379d26eb20fd22e6e10537c1c5ec74c5d1"> + <source>Save Changes</source> + <context-group purpose="location"> + <context context-type="sourcefile">app/student/account/edit-profile/edit-profile.component.html</context> + <context context-type="linenumber">57</context> + </context-group> <context-group purpose="location"> <context context-type="sourcefile">app/teacher/account/edit-profile/edit-profile.component.html</context> - <context context-type="linenumber">108</context> + <context context-type="linenumber">125</context> </context-group> </trans-unit> - <trans-unit datatype="html" id="afa960379d26eb20fd22e6e10537c1c5ec74c5d1"> - <source>Save Changes</source> + <trans-unit datatype="html" id="edb53ba7560c1571d41ff16d93805243e5264d70"> + <source>This profile is linked to a Google account.</source> <context-group purpose="location"> <context context-type="sourcefile">app/student/account/edit-profile/edit-profile.component.html</context> - <context context-type="linenumber">53</context> + <context context-type="linenumber">65</context> </context-group> <context-group purpose="location"> <context context-type="sourcefile">app/teacher/account/edit-profile/edit-profile.component.html</context> - <context context-type="linenumber">120</context> + <context context-type="linenumber">133</context> </context-group> </trans-unit> <trans-unit datatype="html" id="2e1aeaf8d183e6846a0463a08c784f472184fcf0"> @@ -5804,6 +5948,13 @@ <context context-type="linenumber">107</context> </context-group> </trans-unit> + <trans-unit datatype="html" id="2b2f9f56dbfbfb71056a8da8c94e8011d837766e"> + <source> Language required </source> + <context-group purpose="location"> + <context context-type="sourcefile">app/teacher/account/edit-profile/edit-profile.component.html</context> + <context context-type="linenumber">108</context> + </context-group> + </trans-unit> <trans-unit datatype="html" id="44fcb249525dd82d1fd32cae51412bcda7ffd765"> <source>Back to Unit Plan</source> <context-group purpose="location"> @@ -6024,6 +6175,14 @@ <context context-type="sourcefile">../../wise5/components/draw/draw-authoring/draw-authoring.component.html</context> <context context-type="linenumber">2</context> </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/discussion/discussion-authoring/discussion-authoring.component.html</context> + <context context-type="linenumber">2</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/label/label-authoring/label-authoring.component.html</context> + <context context-type="linenumber">2</context> + </context-group> <context-group purpose="location"> <context context-type="sourcefile">../../wise5/components/match/match-authoring/match-authoring.component.html</context> <context context-type="linenumber">2</context> @@ -6036,6 +6195,10 @@ <context context-type="sourcefile">../../wise5/components/openResponse/open-response-authoring/open-response-authoring.component.html</context> <context context-type="linenumber">11</context> </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/table/table-authoring/table-authoring.component.html</context> + <context context-type="linenumber">2</context> + </context-group> </trans-unit> <trans-unit datatype="html" id="495b53dee6164e1c3ec7d95ee3527bb23b62a8c2"> <source>Enter Prompt Here</source> @@ -6047,6 +6210,14 @@ <context context-type="sourcefile">../../wise5/components/draw/draw-authoring/draw-authoring.component.html</context> <context context-type="linenumber">6</context> </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/discussion/discussion-authoring/discussion-authoring.component.html</context> + <context context-type="linenumber">6</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/label/label-authoring/label-authoring.component.html</context> + <context context-type="linenumber">6</context> + </context-group> <context-group purpose="location"> <context context-type="sourcefile">../../wise5/components/match/match-authoring/match-authoring.component.html</context> <context context-type="linenumber">6</context> @@ -6059,6 +6230,10 @@ <context context-type="sourcefile">../../wise5/components/openResponse/open-response-authoring/open-response-authoring.component.html</context> <context context-type="linenumber">15</context> </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/table/table-authoring/table-authoring.component.html</context> + <context context-type="linenumber">6</context> + </context-group> </trans-unit> <trans-unit datatype="html" id="818444ddcd112e756ab725357dcfb238b9257324"> <source>Background Image (Optional)</source> @@ -6105,6 +6280,10 @@ <context context-type="sourcefile">../../wise5/components/draw/draw-authoring/draw-authoring.component.html</context> <context context-type="linenumber">228</context> </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/label/label-authoring/label-authoring.component.html</context> + <context context-type="linenumber">21</context> + </context-group> <context-group purpose="location"> <context context-type="sourcefile">../../wise5/components/match/match-authoring/match-authoring.component.html</context> <context context-type="linenumber">41</context> @@ -6189,6 +6368,10 @@ <context context-type="sourcefile">../../wise5/components/conceptMap/concept-map-authoring/concept-map-authoring.component.html</context> <context context-type="linenumber">87</context> </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/label/label-authoring/label-authoring.component.html</context> + <context context-type="linenumber">24</context> + </context-group> <context-group purpose="location"> <context context-type="sourcefile">../../wise5/components/multipleChoice/multiple-choice-authoring/multiple-choice-authoring.component.html</context> <context context-type="linenumber">68</context> @@ -6316,6 +6499,14 @@ <context context-type="sourcefile">app/authoring-tool/edit-component-tags/edit-component-tags.component.html</context> <context context-type="linenumber">38</context> </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/label/label-authoring/label-authoring.component.html</context> + <context context-type="linenumber">206</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/label/label-authoring/label-authoring.component.html</context> + <context context-type="linenumber">232</context> + </context-group> <context-group purpose="location"> <context context-type="sourcefile">../../wise5/components/match/match-authoring/match-authoring.component.html</context> <context context-type="linenumber">79</context> @@ -6332,6 +6523,14 @@ <context context-type="sourcefile">../../wise5/components/multipleChoice/multiple-choice-authoring/multiple-choice-authoring.component.html</context> <context context-type="linenumber">124</context> </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/table/table-authoring/table-authoring.component.html</context> + <context context-type="linenumber">61</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/table/table-authoring/table-authoring.component.html</context> + <context context-type="linenumber">102</context> + </context-group> </trans-unit> <trans-unit datatype="html" id="e3b469b69eef004764c51b7a8b3efa761501ab73"> <source> Show Node Labels </source> @@ -6378,6 +6577,10 @@ <context context-type="sourcefile">../../wise5/components/conceptMap/concept-map-authoring/concept-map-authoring.component.html</context> <context context-type="linenumber">195</context> </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/label/label-authoring/label-authoring.component.html</context> + <context context-type="linenumber">128</context> + </context-group> </trans-unit> <trans-unit datatype="html" id="9fae747b3706d7f4d6b694ae273d7df2799a732c"> <source>(Optional) Create a starting state for the concept map by editing the "Student Preview" below and then saving here:</source> @@ -6613,6 +6816,10 @@ <context context-type="sourcefile">../../wise5/components/draw/draw-authoring/draw-authoring.component.html</context> <context context-type="linenumber">277</context> </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/label/label-authoring/label-authoring.component.html</context> + <context context-type="linenumber">221</context> + </context-group> </trans-unit> <trans-unit datatype="html" id="a5a5b66acab96c55cf12967a05d09f0ad6524482"> <source>Delete Starter Drawing</source> @@ -6621,6 +6828,20 @@ <context context-type="linenumber">285</context> </context-group> </trans-unit> + <trans-unit datatype="html" id="1037affb09c3a77f83838b47e805b78b82314e6c"> + <source> Students can upload and use images in their posts </source> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/discussion/discussion-authoring/discussion-authoring.component.html</context> + <context context-type="linenumber">16</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="4b10143a37c7361d1cc9c3ffc72e792e99ffebda"> + <source> Students must create a post before viewing classmates' posts </source> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/discussion/discussion-authoring/discussion-authoring.component.html</context> + <context context-type="linenumber">25</context> + </context-group> + </trans-unit> <trans-unit datatype="html" id="7affaae0e14529c0192d5a4bfabd6fcb1ea27529"> <source>Rubric</source> <context-group purpose="location"> @@ -6691,6 +6912,196 @@ <context context-type="linenumber">2</context> </context-group> </trans-unit> + <trans-unit datatype="html" id="b8c5c01ab5965f003ba27c1aad429045431ba260"> + <source>Background Image</source> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/label/label-authoring/label-authoring.component.html</context> + <context context-type="linenumber">13</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="33fa790e0ddac9013da2c3c2016b0c015fc20e6c"> + <source>Canvas Width (px)</source> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/label/label-authoring/label-authoring.component.html</context> + <context context-type="linenumber">31</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="579bd990c53c37a00d6f2be19b772c2bd901b5eb"> + <source>Canvas Height (px)</source> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/label/label-authoring/label-authoring.component.html</context> + <context context-type="linenumber">38</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="19858049294089faa3ac7a89cd9fd8cb7c0cb084"> + <source>Point Radius Size (px)</source> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/label/label-authoring/label-authoring.component.html</context> + <context context-type="linenumber">47</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="0bd1b7ca428a0c47d189e1f26194062b13a0aa6c"> + <source>Font Size</source> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/label/label-authoring/label-authoring.component.html</context> + <context context-type="linenumber">54</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="121f675f133037351ec1ab755f28255087d9a8d0"> + <source>Label Max Character Width</source> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/label/label-authoring/label-authoring.component.html</context> + <context context-type="linenumber">61</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="a810bf354768354951108e32fa7e876345690b41"> + <source> Can Student Create Labels </source> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/label/label-authoring/label-authoring.component.html</context> + <context context-type="linenumber">74</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="ba37ce872dd59293b716f4aab9ffa2dcacef8720"> + <source> Enable Dots </source> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/label/label-authoring/label-authoring.component.html</context> + <context context-type="linenumber">83</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="f89b35f27f0d1b5e3a9d45a5440a7646af2499b9"> + <source> Allow Student to Upload Image for Background </source> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/label/label-authoring/label-authoring.component.html</context> + <context context-type="linenumber">92</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="720864f262b53e2fff90515031217f60a6f6f6fc"> + <source>Starter Labels</source> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/label/label-authoring/label-authoring.component.html</context> + <context context-type="linenumber">99</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="c731b40da1d127154343d68fc55d26306861ac21"> + <source>Add Starter Label</source> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/label/label-authoring/label-authoring.component.html</context> + <context context-type="linenumber">103</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="a11ec4e640f241329943237d2a3cceaa4823658e"> + <source>add</source> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/label/label-authoring/label-authoring.component.html</context> + <context context-type="linenumber">106</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="576fe3dc92e73a8a329cf2fbd1ec8557b37446e1"> + <source> There are no starter labels. Click the "Add Label" button to add a starter label. </source> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/label/label-authoring/label-authoring.component.html</context> + <context context-type="linenumber">113</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="8ba143716c2da6e4120c0c1a804f0bdd9a7e5f5b"> + <source>Text</source> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/label/label-authoring/label-authoring.component.html</context> + <context context-type="linenumber">120</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="8cf83753f4aa4dcef3ace7c49f8501c15d2951d2"> + <source>Enter Label Text Here</source> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/label/label-authoring/label-authoring.component.html</context> + <context context-type="linenumber">124</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="5040677be611ec050369ecb2b9c1501219b9a5d1"> + <source>View Colors</source> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/label/label-authoring/label-authoring.component.html</context> + <context context-type="linenumber">137</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="e534bce868110db32f1c6d91a123548048dc66c1"> + <source>Color Palette</source> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/label/label-authoring/label-authoring.component.html</context> + <context context-type="linenumber">140</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="b2170964421e8fb291a89d020602cc92cb4eaf56"> + <source> Can Student Edit Label </source> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/label/label-authoring/label-authoring.component.html</context> + <context context-type="linenumber">151</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="5005a394c03499dd6211c362e47d7d1c6987a020"> + <source> Can Student Delete Label </source> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/label/label-authoring/label-authoring.component.html</context> + <context context-type="linenumber">160</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="d70bab20ade1a98b907f11f54c14a2c9b9012605"> + <source>Point Location</source> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/label/label-authoring/label-authoring.component.html</context> + <context context-type="linenumber">167</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="eb652ec6e5b2ef867985722c158224053cca15ab"> + <source>X</source> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/label/label-authoring/label-authoring.component.html</context> + <context context-type="linenumber">169</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/label/label-authoring/label-authoring.component.html</context> + <context context-type="linenumber">186</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="5f735468df8e1a39a74409adff2c9a9a5f29ead8"> + <source>Y</source> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/label/label-authoring/label-authoring.component.html</context> + <context context-type="linenumber">176</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/label/label-authoring/label-authoring.component.html</context> + <context context-type="linenumber">193</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="054b7124def3fc0eacfb779c0d98a6dc4dec2ae3"> + <source>Text Location</source> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/label/label-authoring/label-authoring.component.html</context> + <context context-type="linenumber">184</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="b5b4ee55539c3229b8eb1f201029aeffcbbd0841"> + <source>Delete Label</source> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/label/label-authoring/label-authoring.component.html</context> + <context context-type="linenumber">203</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="f1e5600c2fa5270b62ebe32c80557986dd7e1fc0"> + <source>Save Starter Labels</source> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/label/label-authoring/label-authoring.component.html</context> + <context context-type="linenumber">218</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="ce8236188975ede0677402fb75456a3db8499613"> + <source>Delete Starter Labels</source> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/label/label-authoring/label-authoring.component.html</context> + <context context-type="linenumber">229</context> + </context-group> + </trans-unit> <trans-unit datatype="html" id="e599864c0a890e5dfad60da9e9ed9f53ca9893bb"> <source>Choices</source> <context-group purpose="location"> @@ -7067,6 +7478,113 @@ <context context-type="linenumber">6</context> </context-group> </trans-unit> + <trans-unit datatype="html" id="b169576ab898458aa823e09c1e7f3397f8f011a0"> + <source>Columns</source> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/table/table-authoring/table-authoring.component.html</context> + <context context-type="linenumber">13</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="79765b4b2ed87b05e0fd5d6937cec32d1995645f"> + <source>Rows</source> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/table/table-authoring/table-authoring.component.html</context> + <context context-type="linenumber">21</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="aa22ffb76fc0555efcd54b896bff75a3e469982e"> + <source>Global Cell Size</source> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/table/table-authoring/table-authoring.component.html</context> + <context context-type="linenumber">29</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="630090c98b3bf8c12d948c30ea150a25bc861fe9"> + <source>Insert Column Before</source> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/table/table-authoring/table-authoring.component.html</context> + <context context-type="linenumber">47</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/table/table-authoring/table-authoring.component.html</context> + <context context-type="linenumber">50</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="9634f970a787b967a6da5d3edcd6d6530012278c"> + <source>Delete Column</source> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/table/table-authoring/table-authoring.component.html</context> + <context context-type="linenumber">58</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="97537cb70f46603f1ae244ad748fa2622fb5bf0b"> + <source>Insert Column After</source> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/table/table-authoring/table-authoring.component.html</context> + <context context-type="linenumber">70</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/table/table-authoring/table-authoring.component.html</context> + <context context-type="linenumber">73</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="897363afbaacb7374bb5e259129b76687b431671"> + <source>Insert Row Before</source> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/table/table-authoring/table-authoring.component.html</context> + <context context-type="linenumber">88</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/table/table-authoring/table-authoring.component.html</context> + <context context-type="linenumber">91</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="2ab75889a800e0e0fd14d0d63b8a003ee69519c6"> + <source>Delete Row</source> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/table/table-authoring/table-authoring.component.html</context> + <context context-type="linenumber">99</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="75e10460c25b4a5f95b9d8077f0cb9b8968b47ce"> + <source>Insert Row After</source> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/table/table-authoring/table-authoring.component.html</context> + <context context-type="linenumber">111</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/table/table-authoring/table-authoring.component.html</context> + <context context-type="linenumber">114</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="13968818aa5f7771fe043f7d614f21c66b21e21a"> + <source> Editable </source> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/table/table-authoring/table-authoring.component.html</context> + <context context-type="linenumber">133</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="5943b22e7e12f461de18c831b7cf94ddf4baf11a"> + <source>Column Cell Size</source> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/table/table-authoring/table-authoring.component.html</context> + <context context-type="linenumber">155</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="8d3659d1c474ee37dde6c775445b585af3b79b56"> + <source> Make All Cells Editable </source> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/table/table-authoring/table-authoring.component.html</context> + <context context-type="linenumber">171</context> + </context-group> + </trans-unit> + <trans-unit datatype="html" id="964c339de395a09a2ea31889d1f3b0e89ffd9107"> + <source> Make All Cells Uneditable </source> + <context-group purpose="location"> + <context context-type="sourcefile">../../wise5/components/table/table-authoring/table-authoring.component.html</context> + <context context-type="linenumber">178</context> + </context-group> + </trans-unit> <trans-unit datatype="html" id="f9c52903219e583ddefb23cd102d2057504ac980"> <source>(Team <x equiv-text="{{workgroupId}}" id="INTERPOLATION"/>)</source> <context-group purpose="location"> diff --git a/src/main/webapp/site/src/style/layout/_section.scss b/src/main/webapp/site/src/style/layout/_section.scss index bc80d059f1..25b5b018a6 100644 --- a/src/main/webapp/site/src/style/layout/_section.scss +++ b/src/main/webapp/site/src/style/layout/_section.scss @@ -20,7 +20,7 @@ } .section__tab { - padding: 24px 0; + padding: 24px 4px; @media (min-width: breakpoint('sm.min')) { padding: 24px 16px; diff --git a/src/main/webapp/wise5/authoringTool/components/component-authoring.component.ts b/src/main/webapp/wise5/authoringTool/components/component-authoring.component.ts index 6634abd0cc..67ae4403ea 100644 --- a/src/main/webapp/wise5/authoringTool/components/component-authoring.component.ts +++ b/src/main/webapp/wise5/authoringTool/components/component-authoring.component.ts @@ -62,6 +62,11 @@ export abstract class ComponentAuthoring { ); } + ngOnDestroy() { + this.componentChangedSubscription.unsubscribe(); + this.starterStateResponseSubscription.unsubscribe(); + } + promptChanged(prompt: string): void { this.promptChange.next(prompt); } @@ -103,6 +108,16 @@ export abstract class ComponentAuthoring { }); } + chooseBackgroundImage(): void { + const params = { + isPopup: true, + nodeId: this.nodeId, + componentId: this.componentId, + target: 'background' + }; + this.openAssetChooser(params); + } + openAssetChooser(params: any): any { return this.ProjectAssetService.openAssetChooser(params).then((data: any) => { return this.assetSelected(data); diff --git a/src/main/webapp/wise5/classroomMonitor/classroomMonitorComponents/milestones/milestoneDetailsDialog/milestoneDetailsDialog.html b/src/main/webapp/wise5/classroomMonitor/classroomMonitorComponents/milestones/milestoneDetailsDialog/milestoneDetailsDialog.html new file mode 100644 index 0000000000..986833bfc1 --- /dev/null +++ b/src/main/webapp/wise5/classroomMonitor/classroomMonitorComponents/milestones/milestoneDetailsDialog/milestoneDetailsDialog.html @@ -0,0 +1,21 @@ +<md-dialog class="dialog--wider" aria-label="{{ 'MILESTONE_DETAILS_TITLE' | translate : { name: $ctrl.milestone.name } }}"> + <md-toolbar> + <div class="md-toolbar-tools"> + <h2>{{ 'MILESTONE_DETAILS_TITLE' | translate : { name: $ctrl.milestone.name } }}</h2> + </div> + </md-toolbar> + <md-dialog-content class="gray-lighter-bg md-dialog-content"> + <milestone-details milestone="$ctrl.milestone" + hide-student-work="$ctrl.hideStudentWork" + on-show-workgroup="$ctrl.onShowWorkgroup(value)" + on-visit-node-grading="$ctrl.onVisitNodeGrading()"></milestone-details> + </md-dialog-content> + <md-dialog-actions layout="row" layout-align="start center"> + <span flex></span> + <md-button class="md-primary" + ng-click="$ctrl.close()" + aria-label="{{ ::'CLOSE' | translate }}"> + {{ ::'CLOSE' | translate }} + </md-button> + </md-dialog-actions> +</md-dialog> \ No newline at end of file diff --git a/src/main/webapp/wise5/classroomMonitor/classroomMonitorComponents/milestones/milestoneDetailsDialog/milestoneDetailsDialog.ts b/src/main/webapp/wise5/classroomMonitor/classroomMonitorComponents/milestones/milestoneDetailsDialog/milestoneDetailsDialog.ts new file mode 100644 index 0000000000..eaec3e2d0b --- /dev/null +++ b/src/main/webapp/wise5/classroomMonitor/classroomMonitorComponents/milestones/milestoneDetailsDialog/milestoneDetailsDialog.ts @@ -0,0 +1,77 @@ +export class MilestoneDetailsDialog { + title: string; + + static $inject = [ + '$state', + '$mdDialog', + '$event', + 'milestone', + 'hideStudentWork', + 'TeacherDataService' + ]; + + constructor( + private $state, + private $mdDialog, + private $event, + private milestone, + private hideStudentWork, + private TeacherDataService + ) {} + + $onInit() { + this.saveMilestoneOpenedEvent(); + } + + close() { + this.saveMilestoneClosedEvent(); + this.$mdDialog.hide(); + } + + edit() { + this.$mdDialog.hide({ + milestone: this.milestone, + action: 'edit', + $event: this.$event + }); + } + + onShowWorkgroup(workgroup: any) { + this.saveMilestoneClosedEvent(); + this.$mdDialog.hide(); + this.TeacherDataService.setCurrentWorkgroup(workgroup); + this.$state.go('root.nodeProgress'); + } + + onVisitNodeGrading() { + this.$mdDialog.hide(); + } + + saveMilestoneOpenedEvent() { + this.saveMilestoneEvent('MilestoneOpened'); + } + + saveMilestoneClosedEvent() { + this.saveMilestoneEvent('MilestoneClosed'); + } + + saveMilestoneEvent(event: any) { + const context = 'ClassroomMonitor', + nodeId = null, + componentId = null, + componentType = null, + category = 'Navigation', + data = { milestoneId: this.milestone.id }, + projectId = null; + this.TeacherDataService.saveEvent( + context, + nodeId, + componentId, + componentType, + category, + event, + data, + projectId + ); + } +} diff --git a/src/main/webapp/wise5/components/conceptMap/concept-map-authoring/concept-map-authoring.component.ts b/src/main/webapp/wise5/components/conceptMap/concept-map-authoring/concept-map-authoring.component.ts index 5a140470a4..9fbf33365e 100644 --- a/src/main/webapp/wise5/components/conceptMap/concept-map-authoring/concept-map-authoring.component.ts +++ b/src/main/webapp/wise5/components/conceptMap/concept-map-authoring/concept-map-authoring.component.ts @@ -156,16 +156,6 @@ export class ConceptMapAuthoring extends ComponentAuthoring { } } - chooseBackgroundImage(): void { - const params = { - isPopup: true, - nodeId: this.nodeId, - componentId: this.componentId, - target: 'background' - }; - this.openAssetChooser(params); - } - chooseNodeImage(conceptMapNodeId: string): void { const params = { isPopup: true, diff --git a/src/main/webapp/wise5/components/discussion/authoring.html b/src/main/webapp/wise5/components/discussion/authoring.html deleted file mode 100644 index 1c169b45ce..0000000000 --- a/src/main/webapp/wise5/components/discussion/authoring.html +++ /dev/null @@ -1,28 +0,0 @@ -<div> - <div ng-if='!discussionController.authoringComponentContent.showPreviousWork'> - <md-input-container style='width: 100%; margin-bottom: 0;'> - <label>{{ ::'PROMPT' | translate }}</label> - <textarea rows='1' - ng-model='discussionController.authoringComponentContent.prompt' - ng-change='discussionController.componentChanged()' - ng-model-options='{ debounce: 1000 }' - placeholder='{{ ::"enterPromptHere" | translate }}'> - </textarea> - </md-input-container> - <md-input-container style='margin-top: 0; margin-bottom: 0;'> - <md-checkbox class='md-primary' - ng-model='discussionController.authoringComponentContent.isStudentAttachmentEnabled' - ng-change='discussionController.componentChanged()'> - {{ ::'discussion.allowUploadedImagesInPosts' | translate }} - </md-checkbox> - </md-input-container> - <br/> - <md-input-container style='margin-top: 0; margin-bottom: 0;'> - <md-checkbox class='md-primary' - ng-model='discussionController.authoringComponentContent.gateClassmateResponses' - ng-change='discussionController.componentChanged()'> - {{ ::'discussion.gateClassmateResponses' | translate }} - </md-checkbox> - </md-input-container> - </div> -</div> diff --git a/src/main/webapp/wise5/components/discussion/discussion-authoring/discussion-authoring.component.html b/src/main/webapp/wise5/components/discussion/discussion-authoring/discussion-authoring.component.html new file mode 100644 index 0000000000..d457e04955 --- /dev/null +++ b/src/main/webapp/wise5/components/discussion/discussion-authoring/discussion-authoring.component.html @@ -0,0 +1,28 @@ +<mat-form-field class="prompt"> + <mat-label i18n>Prompt</mat-label> + <textarea matInput + [ngModel]="authoringComponentContent.prompt" + (ngModelChange)="promptChanged($event)" + placeholder="Enter Prompt Here" + i18n-placeholder + cdkTextareaAutosize> + </textarea> +</mat-form-field> +<div class="checkbox-container"> + <mat-checkbox + color="primary" + [(ngModel)]="authoringComponentContent.isStudentAttachmentEnabled" + (ngModelChange)="componentChanged()" + i18n> + Students can upload and use images in their posts + </mat-checkbox> +</div> +<div class="checkbox-container"> + <mat-checkbox + color="primary" + [(ngModel)]="authoringComponentContent.gateClassmateResponses" + (ngModelChange)="componentChanged()" + i18n> + Students must create a post before viewing classmates' posts + </mat-checkbox> +</div> \ No newline at end of file diff --git a/src/main/webapp/wise5/components/discussion/discussion-authoring/discussion-authoring.component.scss b/src/main/webapp/wise5/components/discussion/discussion-authoring/discussion-authoring.component.scss new file mode 100644 index 0000000000..7221a74cf4 --- /dev/null +++ b/src/main/webapp/wise5/components/discussion/discussion-authoring/discussion-authoring.component.scss @@ -0,0 +1,8 @@ +.prompt { + width: 100%; +} + +.checkbox-container { + margin-top: 5px; + margin-bottom: 15px; +} \ No newline at end of file diff --git a/src/main/webapp/wise5/components/discussion/discussion-authoring/discussion-authoring.component.ts b/src/main/webapp/wise5/components/discussion/discussion-authoring/discussion-authoring.component.ts new file mode 100644 index 0000000000..ce81455ece --- /dev/null +++ b/src/main/webapp/wise5/components/discussion/discussion-authoring/discussion-authoring.component.ts @@ -0,0 +1,24 @@ +'use strict'; + +import { Component } from '@angular/core'; +import { ProjectAssetService } from '../../../../site/src/app/services/projectAssetService'; +import { ComponentAuthoring } from '../../../authoringTool/components/component-authoring.component'; +import { ConfigService } from '../../../services/configService'; +import { NodeService } from '../../../services/nodeService'; +import { TeacherProjectService } from '../../../services/teacherProjectService'; + +@Component({ + selector: 'discussion-authoring', + templateUrl: 'discussion-authoring.component.html', + styleUrls: ['discussion-authoring.component.scss'] +}) +export class DiscussionAuthoring extends ComponentAuthoring { + constructor( + protected ConfigService: ConfigService, + protected NodeService: NodeService, + protected ProjectAssetService: ProjectAssetService, + protected ProjectService: TeacherProjectService + ) { + super(ConfigService, NodeService, ProjectAssetService, ProjectService); + } +} diff --git a/src/main/webapp/wise5/components/discussion/discussionAuthoring.ts b/src/main/webapp/wise5/components/discussion/discussionAuthoring.ts deleted file mode 100644 index d30b4b0c2f..0000000000 --- a/src/main/webapp/wise5/components/discussion/discussionAuthoring.ts +++ /dev/null @@ -1,49 +0,0 @@ -'use strict'; - -import { Directive } from '@angular/core'; -import { EditComponentController } from '../../authoringTool/components/editComponentController'; - -@Directive() -class DiscussionAuthoringController extends EditComponentController { - static $inject = [ - '$filter', - 'ConfigService', - 'NodeService', - 'NotificationService', - 'ProjectAssetService', - 'ProjectService', - 'UtilService' - ]; - - constructor( - $filter, - ConfigService, - NodeService, - NotificationService, - ProjectAssetService, - ProjectService, - UtilService - ) { - super( - $filter, - ConfigService, - NodeService, - NotificationService, - ProjectAssetService, - ProjectService, - UtilService - ); - } -} - -const DiscussionAuthoring = { - bindings: { - nodeId: '@', - componentId: '@' - }, - controller: DiscussionAuthoringController, - controllerAs: 'discussionController', - templateUrl: 'wise5/components/discussion/authoring.html' -}; - -export default DiscussionAuthoring; diff --git a/src/main/webapp/wise5/components/discussion/discussionAuthoringComponentModule.ts b/src/main/webapp/wise5/components/discussion/discussionAuthoringComponentModule.ts index 77bf783b60..9acf77bcbe 100644 --- a/src/main/webapp/wise5/components/discussion/discussionAuthoringComponentModule.ts +++ b/src/main/webapp/wise5/components/discussion/discussionAuthoringComponentModule.ts @@ -1,16 +1,19 @@ 'use strict'; import * as angular from 'angular'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import { downgradeComponent, downgradeInjectable } from '@angular/upgrade/static'; import { DiscussionService } from './discussionService'; -import DiscussionAuthoring from './discussionAuthoring'; import { EditDiscussionAdvancedComponent } from './edit-discussion-advanced/edit-discussion-advanced.component'; +import { DiscussionAuthoring } from './discussion-authoring/discussion-authoring.component'; const discussionAuthoringComponentModule = angular .module('discussionAuthoringComponentModule', ['pascalprecht.translate']) .service('DiscussionService', downgradeInjectable(DiscussionService)) - .component('discussionAuthoring', DiscussionAuthoring) .component('editDiscussionAdvanced', EditDiscussionAdvancedComponent) + .directive( + 'discussionAuthoring', + downgradeComponent({ component: DiscussionAuthoring }) as angular.IDirectiveFactory + ) .config([ '$translatePartialLoaderProvider', ($translatePartialLoaderProvider) => { diff --git a/src/main/webapp/wise5/components/discussion/discussionService.ts b/src/main/webapp/wise5/components/discussion/discussionService.ts index 7249536a45..78467bf01e 100644 --- a/src/main/webapp/wise5/components/discussion/discussionService.ts +++ b/src/main/webapp/wise5/components/discussion/discussionService.ts @@ -39,7 +39,7 @@ export class DiscussionService extends ComponentService { createComponent() { const component: any = super.createComponent(); component.type = 'Discussion'; - component.prompt = this.getTranslation('ENTER_PROMPT_HERE'); + component.prompt = ''; component.isStudentAttachmentEnabled = true; component.gateClassmateResponses = true; return component; diff --git a/src/main/webapp/wise5/components/draw/draw-authoring/draw-authoring.component.ts b/src/main/webapp/wise5/components/draw/draw-authoring/draw-authoring.component.ts index 930150944a..c17659ba9a 100644 --- a/src/main/webapp/wise5/components/draw/draw-authoring/draw-authoring.component.ts +++ b/src/main/webapp/wise5/components/draw/draw-authoring/draw-authoring.component.ts @@ -176,16 +176,6 @@ export class DrawAuthoring extends ComponentAuthoring { this.componentChanged(); } - chooseBackgroundImage(): void { - const params = { - isPopup: true, - nodeId: this.nodeId, - componentId: this.componentId, - target: 'background' - }; - this.openAssetChooser(params); - } - chooseStampImage(stampIndex: number): void { const params = { isPopup: true, diff --git a/src/main/webapp/wise5/components/label/authoring.html b/src/main/webapp/wise5/components/label/authoring.html deleted file mode 100644 index 2e53f91e8a..0000000000 --- a/src/main/webapp/wise5/components/label/authoring.html +++ /dev/null @@ -1,373 +0,0 @@ -<style> - {{labelController.nodeContent.style}} - .studentButton { - min-width: 50px; - max-width: 50px; - } -</style> - -<div flex> - <div class='advancedAuthoringDiv' - ng-if='labelController.showAdvancedAuthoring'> - <div> - <md-checkbox class='md-primary' - ng-model='labelController.authoringComponentContent.showSaveButton' - ng-change='labelController.componentChanged()'> - {{ ::'SHOW_SAVE_BUTTON' | translate }} - </md-checkbox> - </div> - <div> - <md-checkbox class='md-primary' - ng-model='labelController.authoringComponentContent.showSubmitButton' - ng-change='labelController.componentChanged()'> - {{ ::'SHOW_SUBMIT_BUTTON' | translate }} - </md-checkbox> - </div> - <div> - <md-checkbox class='md-primary' - ng-if='labelController.isNotebookEnabled()' - ng-model='labelController.authoringComponentContent.showAddToNotebookButton' - ng-change='labelController.componentChanged()'> - {{ ::'SHOW_ADD_TO_NOTEBOOK_BUTTON' | translate }} - </md-checkbox> - </div> - <div> - <md-input-container style='margin-right: 20px; width: 150px; height: 25px;' - ng-if='labelController.authoringComponentContent.showSubmitButton'> - <label>{{ ::'MAX_SUBMIT' | translate }}</label> - <input type='number' - ng-model='labelController.authoringComponentContent.maxSubmitCount' - ng-model-options='{ debounce: 1000 }' - ng-change='labelController.componentChanged()'/> - </md-input-container> - </div> - <div layout="column" layout-align="start start"> - <edit-component-max-score [authoring-component-content]="labelController.authoringComponentContent"></edit-component-max-score> - <edit-component-width [authoring-component-content]="labelController.authoringComponentContent"></edit-component-width> - <edit-component-rubric [authoring-component-content]="labelController.authoringComponentContent"></edit-component-rubric> - <edit-component-tags [authoring-component-content]="labelController.authoringComponentContent"></edit-component-tags> - </div> - <div> - <div> - <label class='node__label--vertical-alignment'> - {{ ::'CONNECTED_COMPONENTS' | translate }} - </label> - <md-button class='topButton md-raised md-primary' - ng-click='labelController.addConnectedComponent()'> - <md-icon>add</md-icon> - <md-tooltip md-direction='top' - class='projectButtonTooltip'> - {{ ::'ADD_CONNECTED_COMPONENT' | translate }} - </md-tooltip> - </md-button> - </div> - <div ng-repeat='connectedComponent in labelController.authoringComponentContent.connectedComponents track by $index' - style='border: 2px solid #dddddd; border-radius: 5px; margin-bottom: 10px; padding: 20px 20px 10px 20px;'> - <div flex> - <md-input-container style='margin-right: 20px; width: 300px;'> - <label>{{ ::'step' | translate }}</label> - <md-select ng-model='connectedComponent.nodeId' - ng-change='labelController.connectedComponentNodeIdChanged(connectedComponent)' - style='width: 300px'> - <md-option ng-repeat='item in labelController.idToOrder | toArray | orderBy : "order"' - value='{{item.$key}}' - ng-if='labelController.isApplicationNode(item.$key)'> - {{ labelController.getNodePositionAndTitleByNodeId(item.$key) }} - </md-option> - </md-select> - </md-input-container> - <md-input-container style='margin-right: 20px; width: 300px;'> - <label>{{ ::'component' | translate }}</label> - <md-select ng-model='connectedComponent.componentId' - ng-change='labelController.connectedComponentComponentIdChanged(connectedComponent)' - style='width: 300px'> - <md-option ng-repeat='(componentIndex, component) in labelController.getComponentsByNodeId(connectedComponent.nodeId)' - value='{{component.id}}' - ng-disabled='!labelController.isConnectedComponentTypeAllowed(component.type) || component.id == labelController.componentId'> - {{ componentIndex + 1 }}. {{ component.type }} - <span ng-if='component.id == labelController.componentId'> - ({{ ::'thisComponent' | translate }}) - </span> - </md-option> - </md-select> - </md-input-container> - <md-input-container style='margin-right: 20px; width: 200px;'> - <label>{{ ::'type' | translate }}</label> - <md-select ng-model='connectedComponent.type' - ng-change='labelController.connectedComponentTypeChanged(connectedComponent)' - style='width: 200px'> - <md-option value='importWork'> - {{ ::'importWork' | translate }} - </md-option> - <md-option value='showWork'> - {{ ::'showWork' | translate }} - </md-option> - </md-select> - </md-input-container> - <span flex></span> - <md-input-container style='margin-left: 20px;'> - <md-button class='topButton md-raised md-primary' - ng-click='labelController.deleteConnectedComponent($index)'> - <md-icon>delete</md-icon> - <md-tooltip md-direction='top' - class='projectButtonTooltip'> - {{ ::'DELETE' | translate }} - </md-tooltip> - </md-button> - </md-input-container> - </div> - <div ng-if='labelController.getConnectedComponentType(connectedComponent) == "OpenResponse"' flex> - <md-input-container style='margin-right: 20px;'> - <md-checkbox class='md-primary' - ng-model='connectedComponent.importWorkAsBackground' - ng-change='labelController.importWorkAsBackgroundClicked(connectedComponent)'> - {{ ::'importWorkAsBackground' | translate }} - </md-checkbox> - </md-input-container> - <div ng-if='connectedComponent.importWorkAsBackground' - style='display: inline;'> - <md-input-container style='margin-right: 20px; width: 200px; height: 30px;'> - <label>{{ ::'label.charactersPerLine' | translate }}</label> - <input ng-model='connectedComponent.charactersPerLine' - ng-model-options='{ debounce: 1000 }' - ng-change='labelController.componentChanged()'/> - </md-input-container> - <md-input-container style='margin-right: 20px width: 200px; height: 30px;'> - <label>{{ ::'label.spaceInbetweenLines' | translate }}</label> - <input ng-model='connectedComponent.spaceInbetweenLines' - ng-model-options='{ debounce: 1000 }' - ng-change='labelController.componentChanged()'/> - </md-input-container> - <md-input-container style='margin-right: 20px width: 100px; height: 30px;'> - <label>{{ ::'label.fontSize' | translate }}</label> - <input ng-model='connectedComponent.fontSize' - ng-model-options='{ debounce: 1000 }' - ng-change='labelController.componentChanged()'/> - </md-input-container> - </div> - </div> - <div ng-if='labelController.getConnectedComponentType(connectedComponent) == "ConceptMap" || labelController.getConnectedComponentType(connectedComponent) == "Draw" || labelController.getConnectedComponentType(connectedComponent) == "Embedded" || labelController.getConnectedComponentType(connectedComponent) == "Graph" || labelController.getConnectedComponentType(connectedComponent) == "Table"' flex> - <md-input-container style='margin-right: 20px;'> - <md-checkbox class='md-primary' - ng-model='connectedComponent.importWorkAsBackground' - ng-change='labelController.importWorkAsBackgroundClicked(connectedComponent)' - ng-disabled='true'> - {{ ::'importWorkAsBackground' | translate }} - </md-checkbox> - </md-input-container> - </div> - </div> - </div> - <edit-component-json [node-id]="labelController.nodeId" [component-id]="labelController.componentId"></edit-component-json> - </div> - <br/> - <div> - <div ng-if='!labelController.authoringComponentContent.showPreviousWork'> - <md-input-container style='width:100%'> - <label>{{ ::'PROMPT' | translate }}</label> - <textarea rows='1' - ng-model='labelController.authoringComponentContent.prompt' - ng-change='labelController.componentChanged()' - ng-model-options='{ debounce: 1000 }' - placeholder='{{ ::"enterPromptHere" | translate }}'> - </textarea> - </md-input-container> - <div style='height: 60px;'> - <md-input-container> - <label>{{ ::'BACKGROUND_IMAGE' | translate }}</label> - <input size='100' - ng-model='labelController.authoringComponentContent.backgroundImage' - ng-change='labelController.componentChanged()'/> - </md-input-container> - <md-button class='topButton md-raised md-primary' - ng-click='labelController.chooseBackgroundImage()'> - <md-icon>insert_photo</md-icon> - <md-tooltip md-direction='top' - class='projectButtonTooltip'> - {{ ::'chooseAnImage' | translate }} - </md-tooltip> - </md-button> - </div> - <div style='height: 60px;'> - <md-input-container> - <label>{{ ::'label.canvasWidth' | translate }}</label> - <input type='number' - style='width: 200px' - ng-model='labelController.authoringComponentContent.width' - ng-change='labelController.componentChanged()'/> - </md-input-container> - <md-input-container> - <label>{{ ::'label.canvasHeight' | translate }}</label> - <input type='number' - style='width: 200px' - ng-model='labelController.authoringComponentContent.height' - ng-change='labelController.componentChanged()'/> - </md-input-container> - </div> - <div style='height: 60px;'> - <md-input-container> - <label>{{ ::'label.pointSize' | translate }}</label> - <input type='number' - style='width: 200px' - ng-model='labelController.authoringComponentContent.pointSize' - ng-change='labelController.componentChanged()'/> - </md-input-container> - <md-input-container> - <label>{{ ::'label.fontSize' | translate }}</label> - <input type='number' - style='width: 200px' - ng-model='labelController.authoringComponentContent.fontSize' - ng-change='labelController.componentChanged()'/> - </md-input-container> - <md-input-container> - <label>{{ ::'label.labelWidth' | translate }}</label> - <input type='number' - style='width: 200px' - ng-model='labelController.authoringComponentContent.labelWidth' - ng-change='labelController.componentChanged()'/> - </md-input-container> - </div> - <div> - <md-checkbox class='md-primary' - ng-model='labelController.authoringComponentContent.canCreateLabels' - ng-change='labelController.componentChanged()'> - {{ ::'label.canStudentCreateLabels' | translate }} - </md-checkbox> - <br/> - <md-checkbox class='md-primary' - ng-model='labelController.authoringComponentContent.enableCircles' - ng-change='labelController.componentChanged()'> - {{ ::'label.enableDots' | translate }} - </md-checkbox> - <br/> - <md-checkbox class='md-primary' - ng-model='labelController.authoringComponentContent.enableStudentUploadBackground' - ng-change='labelController.componentChanged()'> - {{ ::'label.allowStudentToUploadImageForBackground' | translate }} - </md-checkbox> - </div> - <div> - <div layout='row'> - <h6>{{ ::'label.starterLabels' | translate }}</h6> - <md-button class='topButton md-raised md-primary' - ng-click='labelController.addLabelClicked()'> - <md-icon>add</md-icon> - <md-tooltip md-direction='top' - class='projectButtonTooltip'> - {{ ::'label.addStarterLabel' | translate }} - </md-tooltip> - </md-button> - </div> - <div ng-if='labelController.authoringComponentContent.labels == null || labelController.authoringComponentContent.labels.length == 0'> - {{ ::'label.thereAreNoStarterLabels' | translate }} - </div> - <div ng-repeat='label in labelController.authoringComponentContent.labels track by $index' - style='border: 2px solid #dddddd; border-radius: 5px; margin-bottom: 10px; padding: 20px 20px 10px 20px;'> - <div style='height: 60px;'> - <md-input-container style='margin-right: 20px;'> - <label>{{ ::'TEXT' | translate }}</label> - <input ng-model='label.text' - ng-change='labelController.componentChanged()' - ng-model-options='{ debounce: 1000 }' - size='50' - placeholder='{{ ::"label.enterLabelTextHere" | translate }}'/> - </md-input-container> - <md-input-container> - <label>{{ ::'label.color' | translate }}</label> - <input ng-model='label.color' - ng-change='labelController.componentChanged()' - ng-model-options='{ debounce: 1000 }'/> - </md-input-container> - <md-button class='topButton md-raised md-primary' - ng-click='labelController.openColorViewer()'> - <md-icon>palette</md-icon> - <md-tooltip md-direction='top' - class='projectButtonTooltip'> - {{ ::'label.viewColors' | translate }} - </md-tooltip> - </md-button> - </div> - <div style='height: 80px; margin-top: 10px;'> - <md-checkbox class='md-primary' - ng-model='label.canEdit' - ng-change='labelController.componentChanged()'> - {{ ::'label.canStudentEditLabel' | translate }} - </md-checkbox> - <br/> - <md-checkbox class='md-primary' - ng-model='label.canDelete' - ng-change='labelController.componentChanged()'> - {{ ::'label.canStudentDeleteLabel' | translate }} - </md-checkbox> - </div> - <div style='height: 60px;' - ng-if='labelController.enableCircles'> - <md-input-container style='margin-right: 20px;'> - <span>{{ ::'label.pointLocation' | translate }}</span> - </md-input-container> - <md-input-container style='margin-right: 20px;'> - <label>{{ ::'label.x' | translate }}</label> - <input type='number' - ng-model='label.pointX' - ng-change='labelController.componentChanged()' - ng-model-options='{ debounce: 1000 }'/> - </md-input-container> - <md-input-container> - <label>{{ ::'label.y' | translate }}</label> - <input type='number' - ng-model='label.pointY' - ng-change='labelController.componentChanged()' - ng-model-options='{ debounce: 1000 }'/> - </md-input-container> - </div> - <div style='height: 60px;' - layout='row'> - <md-input-container style='margin-right: 20px;'> - <span>{{ ::'label.textLocation' | translate }}</span> - </md-input-container> - <md-input-container style='margin-right: 20px;'> - <label>{{ ::'label.x' | translate }}</label> - <input type='number' - ng-model='label.textX' - ng-change='labelController.componentChanged()' - ng-model-options='{ debounce: 1000 }'/> - </md-input-container> - <md-input-container> - <label>{{ ::'label.y' | translate }}</label> - <input type='number' - ng-model='label.textY' - ng-change='labelController.componentChanged()' - ng-model-options='{ debounce: 1000 }'/> - </md-input-container> - <span flex></span> - <md-button class='moveComponentButton md-raised md-primary' - style='margin-top: 20px;' - ng-click='labelController.deleteLabelClicked($index, label)'> - <md-icon>delete</md-icon> - <md-tooltip md-direction='top' - class='projectButtonTooltip'> - {{ ::'label.deleteLabel' | translate }} - </md-tooltip> - </md-button> - </div> - </div> - </div> - <md-button class='topButton md-raised md-primary' - ng-click='labelController.saveStarterLabels()'> - <md-icon>create</md-icon> - <md-tooltip md-direction='top' - class='projectButtonTooltip'> - {{ ::'label.saveStarterLabels' | translate }} - </md-tooltip> - </md-button> - <md-button class='topButton md-raised md-primary' - ng-click='labelController.deleteStarterLabels()'> - <md-icon>delete_sweep</md-icon> - <md-tooltip md-direction='top' - class='projectButtonTooltip'> - {{ ::'label.deleteStarterLabels' | translate }} - </md-tooltip> - </md-button> - </div> - </div> -</div> diff --git a/src/main/webapp/wise5/components/label/label-authoring/label-authoring.component.html b/src/main/webapp/wise5/components/label/label-authoring/label-authoring.component.html new file mode 100644 index 0000000000..9aa57edb6b --- /dev/null +++ b/src/main/webapp/wise5/components/label/label-authoring/label-authoring.component.html @@ -0,0 +1,236 @@ +<mat-form-field class="prompt"> + <mat-label i18n>Prompt</mat-label> + <textarea matInput + [ngModel]="authoringComponentContent.prompt" + (ngModelChange)="promptChanged($event)" + placeholder="Enter Prompt Here" + i18n-placeholder + cdkTextareaAutosize> + </textarea> +</mat-form-field> +<div> + <mat-form-field class="background-image input"> + <mat-label i18n>Background Image</mat-label> + <input matInput + [(ngModel)]="authoringComponentContent.backgroundImage" + (ngModelChange)="textInputChange.next($event)"/> + </mat-form-field> + <button mat-raised-button + color="primary" + (click)="chooseBackgroundImage()" + matTooltip="Choose an Image" + matTooltipPosition="above" + i18n-matTooltip + aria-label="Image" + i18n-aria-label> + <mat-icon>insert_photo</mat-icon> + </button> +</div> +<div> + <mat-form-field class="input"> + <mat-label i18n>Canvas Width (px)</mat-label> + <input matInput + type="number" + [(ngModel)]="authoringComponentContent.width" + (ngModelChange)="numberInputChange.next($event)"/> + </mat-form-field> + <mat-form-field class="input"> + <mat-label i18n>Canvas Height (px)</mat-label> + <input matInput + type="number" + [(ngModel)]="authoringComponentContent.height" + (ngModelChange)="numberInputChange.next($event)"/> + </mat-form-field> +</div> +<div> + <mat-form-field class="input"> + <mat-label i18n>Point Radius Size (px)</mat-label> + <input matInput + type="number" + [(ngModel)]="authoringComponentContent.pointSize" + (ngModelChange)="numberInputChange.next($event)"/> + </mat-form-field> + <mat-form-field class="input"> + <mat-label i18n>Font Size</mat-label> + <input matInput + type="number" + [(ngModel)]="authoringComponentContent.fontSize" + (ngModelChange)="numberInputChange.next($event)"/> + </mat-form-field> + <mat-form-field class="input"> + <mat-label i18n>Label Max Character Width</mat-label> + <input matInput + type="number" + [(ngModel)]="authoringComponentContent.labelWidth" + (ngModelChange)="numberInputChange.next($event)"/> + </mat-form-field> +</div> +<div> + <div class="checkbox"> + <mat-checkbox + color="primary" + [(ngModel)]="authoringComponentContent.canCreateLabels" + (change)="componentChanged()" + i18n> + Can Student Create Labels + </mat-checkbox> + </div> + <div class="checkbox"> + <mat-checkbox + color="primary" + [(ngModel)]="authoringComponentContent.enableCircles" + (ngModelChange)="componentChanged()" + i18n> + Enable Dots + </mat-checkbox> + </div> + <div class="checkbox"> + <mat-checkbox + color="primary" + [(ngModel)]="authoringComponentContent.enableStudentUploadBackground" + (ngModelChange)="componentChanged()" + i18n> + Allow Student to Upload Image for Background + </mat-checkbox> + </div> +</div> +<div> + <div layout="row" class="starter-labels-button-container"> + <span i18n class="starter-labels-button-label">Starter Labels</span> + <button mat-raised-button + color="primary" + (click)="addLabel()" + matTooltip="Add Starter Label" + matTooltipPosition="above" + i18n-matTooltip + aria-label="add" + i18n-aria-label> + <mat-icon>add</mat-icon> + </button> + </div> + <div *ngIf="authoringComponentContent.labels == null || authoringComponentContent.labels.length === 0" + class="info-block" + i18n> + There are no starter labels. Click the "Add Label" button to add a starter label. + </div> + <div *ngFor="let label of authoringComponentContent.labels; index as labelIndex" + class="starter-label-container"> + <div> + <mat-form-field class="input label-input"> + <mat-label i18n>Text</mat-label> + <input matInput + [(ngModel)]="label.text" + (ngModelChange)="textInputChange.next($event)" + placeholder="Enter Label Text Here" + i18n-placeholder/> + </mat-form-field> + <mat-form-field class="input color-input"> + <mat-label i18n>Color</mat-label> + <input matInput + [(ngModel)]="label.color" + (ngModelChange)="textInputChange.next($event)" + ng-model-options="{ debounce: 1000 }"/> + </mat-form-field> + <button mat-raised-button + color="primary" + (click)="openColorViewer()" + matTooltip="View Colors" + matTooltipPosition="above" + i18n-matTooltip + aria-label="Color Palette" + i18n-aria-label> + <mat-icon>palette</mat-icon> + </button> + </div> + <div> + <div class="checkbox"> + <mat-checkbox + color="primary" + [(ngModel)]="label.canEdit" + (ngModelChange)="componentChanged()" + i18n> + Can Student Edit Label + </mat-checkbox> + </div> + <div class="checkbox"> + <mat-checkbox + color="primary" + [(ngModel)]="label.canDelete" + (ngModelChange)="componentChanged()" + i18n> + Can Student Delete Label + </mat-checkbox> + </div> + </div> + <div *ngIf="authoringComponentContent.enableCircles" + fxLayout="row wrap" fxLayoutAlign="start center"> + <span class="coordinate-location-label" i18n>Point Location</span> + <mat-form-field class="input coordinate-input"> + <mat-label i18n>X</mat-label> + <input matInput + type="number" + [(ngModel)]="label.pointX" + (ngModelChange)="numberInputChange.next($event)"/> + </mat-form-field> + <mat-form-field class="input coordinate-input"> + <mat-label i18n>Y</mat-label> + <input matInput + type="number" + [(ngModel)]="label.pointY" + (ngModelChange)="numberInputChange.next($event)"/> + </mat-form-field> + </div> + <div fxLayout="row wrap" fxLayoutAlign="start center"> + <span class="coordinate-location-label" i18n>Text Location</span> + <mat-form-field class="input coordinate-input"> + <mat-label i18n>X</mat-label> + <input matInput + type="number" + [(ngModel)]="label.textX" + (ngModelChange)="numberInputChange.next($event)"/> + </mat-form-field> + <mat-form-field class="input coordinate-input"> + <mat-label i18n>Y</mat-label> + <input matInput + type="number" + [(ngModel)]="label.textY" + (ngModelChange)="numberInputChange.next($event)"/> + </mat-form-field> + <span fxFlex></span> + <button mat-raised-button + color="primary" + (click)="deleteLabel(labelIndex, label)" + matTooltip="Delete Label" + matTooltipPosition="above" + i18n-matTooltip + aria-label="Delete" + i18n-aria-label> + <mat-icon>delete</mat-icon> + </button> + </div> + </div> +</div> +<div class="starter-labels-buttons-container"> + <button mat-raised-button + color="primary" + class="starter-labels-button" + (click)="saveStarterLabels()" + matTooltip="Save Starter Labels" + matTooltipPosition="above" + i18n-matTooltip + aria-label="Save" + i18n-aria-label> + <mat-icon>create</mat-icon> + </button> + <button mat-raised-button + color="primary" + class="starter-labels-button" + (click)="deleteStarterLabels()" + matTooltip="Delete Starter Labels" + matTooltipPosition="above" + i18n-matTooltip + aria-label="Delete" + i18n-aria-label> + <mat-icon>delete_sweep</mat-icon> + </button> +</div> diff --git a/src/main/webapp/wise5/components/label/label-authoring/label-authoring.component.scss b/src/main/webapp/wise5/components/label/label-authoring/label-authoring.component.scss new file mode 100644 index 0000000000..2115fcb9d5 --- /dev/null +++ b/src/main/webapp/wise5/components/label/label-authoring/label-authoring.component.scss @@ -0,0 +1,64 @@ +.prompt { + width: 100%; +} + +.background-image { + width: 80%; +} + +.input { + margin-right: 20px; +} + +.checkbox { + margin-top: 10px; + margin-bottom: 10px; +} + +.starter-labels-button-container { + margin-top: 20px; + margin-bottom: 20px; +} + +.starter-labels-button-label { + margin-right: 10px; +} + +.starter-label-container { + border: 2px solid #dddddd; + border-radius: 5px; + margin-bottom: 10px; + padding: 20px 20px 10px 20px; +} + +.info-block { + margin-bottom: 20px; + text-align: center; + font-weight: 500; +} + +.label-input { + width: 40%; +} + +.color-input { + width: 30%; +} + +.coordinate-location-label { + margin-right: 20px; +} + +.coordinate-input { + width: 15%; +} + +.starter-labels-buttons-container { + margin-top: 20px; + margin-bottom: 10px; + margin-left: 10px; +} + +.starter-labels-button { + margin-right: 10px; +} \ No newline at end of file diff --git a/src/main/webapp/wise5/components/label/label-authoring/label-authoring.component.ts b/src/main/webapp/wise5/components/label/label-authoring/label-authoring.component.ts new file mode 100644 index 0000000000..2c2ea4ff2b --- /dev/null +++ b/src/main/webapp/wise5/components/label/label-authoring/label-authoring.component.ts @@ -0,0 +1,124 @@ +'use strict'; + +import { Component } from '@angular/core'; +import { ComponentAuthoring } from '../../../authoringTool/components/component-authoring.component'; +import { ConfigService } from '../../../services/configService'; +import { NodeService } from '../../../services/nodeService'; +import { ProjectAssetService } from '../../../../site/src/app/services/projectAssetService'; +import { TeacherProjectService } from '../../../services/teacherProjectService'; +import { Subject, Subscription } from 'rxjs'; +import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; + +@Component({ + selector: 'label-authoring', + templateUrl: 'label-authoring.component.html', + styleUrls: ['label-authoring.component.scss'] +}) +export class LabelAuthoring extends ComponentAuthoring { + numberInputChange: Subject<number> = new Subject<number>(); + textInputChange: Subject<string> = new Subject<string>(); + + numberInputChangeSubscription: Subscription; + textInputChangeSubscription: Subscription; + + constructor( + protected ConfigService: ConfigService, + protected NodeService: NodeService, + protected ProjectAssetService: ProjectAssetService, + protected ProjectService: TeacherProjectService + ) { + super(ConfigService, NodeService, ProjectAssetService, ProjectService); + this.numberInputChangeSubscription = this.numberInputChange + .pipe(debounceTime(1000), distinctUntilChanged()) + .subscribe(() => { + this.componentChanged(); + }); + this.textInputChangeSubscription = this.textInputChange + .pipe(debounceTime(1000), distinctUntilChanged()) + .subscribe(() => { + this.componentChanged(); + }); + } + + ngOnInit() { + super.ngOnInit(); + if (this.authoringComponentContent.enableCircles == null) { + // If this component was created before enableCircles was implemented, we will default it to + // true in the authoring so that the "Enable Dots" checkbox is checked. + this.authoringComponentContent.enableCircles = true; + } + } + + ngOnDestroy() { + super.ngOnDestroy(); + this.unsubscribeAll(); + } + + unsubscribeAll() { + this.numberInputChangeSubscription.unsubscribe(); + this.textInputChangeSubscription.unsubscribe(); + } + + addLabel(): void { + const newLabel = { + text: $localize`Enter text here`, + color: 'blue', + pointX: 100, + pointY: 100, + textX: 200, + textY: 200, + canEdit: false, + canDelete: false + }; + this.authoringComponentContent.labels.push(newLabel); + this.componentChanged(); + } + + deleteLabel(index: number, label: any): void { + if (confirm($localize`Are you sure you want to delete this label?\n\n${label.text}`)) { + this.authoringComponentContent.labels.splice(index, 1); + this.componentChanged(); + } + } + + assetSelected({ nodeId, componentId, assetItem, target }): void { + super.assetSelected({ nodeId, componentId, assetItem, target }); + const fileName = assetItem.fileName; + if (target === 'background') { + this.authoringComponentContent.backgroundImage = fileName; + this.componentChanged(); + } + } + + saveStarterLabels(): void { + if (confirm($localize`Are you sure you want to save the starter labels?`)) { + this.NodeService.requestStarterState({ nodeId: this.nodeId, componentId: this.componentId }); + } + } + + saveStarterState(starterState: any): void { + this.authoringComponentContent.labels = starterState; + this.componentChanged(); + } + + compareTextAlphabetically(stringA: string, stringB: string) { + if (stringA < stringB) { + return -1; + } else if (stringA > stringB) { + return 1; + } else { + return 0; + } + } + + deleteStarterLabels(): void { + if (confirm($localize`label.areYouSureYouWantToDeleteAllTheStarterLabels`)) { + this.authoringComponentContent.labels = []; + this.componentChanged(); + } + } + + openColorViewer(): void { + window.open('http://www.javascripter.net/faq/colornam.htm'); + } +} diff --git a/src/main/webapp/wise5/components/label/labelAuthoring.ts b/src/main/webapp/wise5/components/label/labelAuthoring.ts deleted file mode 100644 index df29a8ac22..0000000000 --- a/src/main/webapp/wise5/components/label/labelAuthoring.ts +++ /dev/null @@ -1,177 +0,0 @@ -'use strict'; - -import * as $ from 'jquery'; -import * as fabric from 'fabric'; -window['fabric'] = fabric.fabric; -import html2canvas from 'html2canvas'; -import { Directive } from '@angular/core'; -import { EditComponentController } from '../../authoringTool/components/editComponentController'; - -@Directive() -class LabelAuthoringController extends EditComponentController { - static $inject = [ - '$filter', - '$window', - 'ConfigService', - 'NodeService', - 'NotificationService', - 'ProjectAssetService', - 'ProjectService', - 'UtilService' - ]; - - constructor( - $filter, - private $window, - ConfigService, - NodeService, - NotificationService, - ProjectAssetService, - ProjectService, - UtilService - ) { - super( - $filter, - ConfigService, - NodeService, - NotificationService, - ProjectAssetService, - ProjectService, - UtilService - ); - } - - $onInit() { - super.$onInit(); - if (this.authoringComponentContent.enableCircles == null) { - /* - * If this component was created before enableCircles was implemented, - * we will default it to true in the authoring so that the - * "Enable Dots" checkbox is checked. - */ - this.authoringComponentContent.enableCircles = true; - } - } - - addLabelClicked(): void { - const newLabel = { - text: this.$translate('label.enterTextHere'), - color: 'blue', - pointX: 100, - pointY: 100, - textX: 200, - textY: 200, - canEdit: false, - canDelete: false - }; - this.authoringComponentContent.labels.push(newLabel); - this.componentChanged(); - } - - /** - * Delete a label in the authoring view - * @param index the index of the label in the labels array - */ - deleteLabelClicked(index: number, label: any): void { - const answer = confirm( - this.$translate('label.areYouSureYouWantToDeleteThisLabel', { - selectedLabelText: label.textString - }) - ); - if (answer) { - this.authoringComponentContent.labels.splice(index, 1); - this.componentChanged(); - } - } - - chooseBackgroundImage(): void { - const params = { - isPopup: true, - nodeId: this.nodeId, - componentId: this.componentId, - target: 'background' - }; - this.openAssetChooser(params); - } - - assetSelected({ nodeId, componentId, assetItem, target }): void { - super.assetSelected({ nodeId, componentId, assetItem, target }); - const fileName = assetItem.fileName; - if (target === 'background') { - this.authoringComponentContent.backgroundImage = fileName; - this.componentChanged(); - } - } - - saveStarterLabels(): void { - if (confirm(this.$translate('label.areYouSureYouWantToSaveTheStarterLabels'))) { - this.NodeService.requestStarterState({ nodeId: this.nodeId, componentId: this.componentId }); - } - } - - saveStarterState(starterState: any): void { - starterState.sort(this.labelTextComparator); - this.authoringComponentContent.labels = starterState; - this.componentChanged(); - } - - /** - * A comparator used to sort labels alphabetically - * It should be used like labels.sort(this.labelTextComparator); - * @param labelA a label object - * @param labelB a label object - * @return -1 if labelA comes before labelB - * 1 if labelB comes after labelB - * 0 of the labels are equal - */ - labelTextComparator(labelA: any, labelB: any): number { - if (labelA.text < labelB.text) { - return -1; - } else if (labelA.text > labelB.text) { - return 1; - } else { - if (labelA.color < labelB.color) { - return -1; - } else if (labelA.color > labelB.color) { - return 1; - } else { - if (labelA.pointX < labelB.pointX) { - return -1; - } else if (labelA.pointX > labelB.pointX) { - return 1; - } else { - if (labelA.pointY < labelB.pointY) { - return -1; - } else if (labelA.pointY > labelB.pointY) { - return 1; - } else { - return 0; - } - } - } - } - } - - deleteStarterLabels(): void { - if (confirm(this.$translate('label.areYouSureYouWantToDeleteAllTheStarterLabels'))) { - this.authoringComponentContent.labels = []; - this.componentChanged(); - } - } - - openColorViewer(): void { - this.$window.open('http://www.javascripter.net/faq/colornam.htm'); - } -} - -const LabelAuthoring = { - bindings: { - nodeId: '@', - componentId: '@' - }, - controller: LabelAuthoringController, - controllerAs: 'labelController', - templateUrl: 'wise5/components/label/authoring.html' -}; - -export default LabelAuthoring; diff --git a/src/main/webapp/wise5/components/label/labelAuthoringComponentModule.ts b/src/main/webapp/wise5/components/label/labelAuthoringComponentModule.ts index cceefafef9..e6bd14a03c 100644 --- a/src/main/webapp/wise5/components/label/labelAuthoringComponentModule.ts +++ b/src/main/webapp/wise5/components/label/labelAuthoringComponentModule.ts @@ -1,15 +1,18 @@ 'use strict'; import * as angular from 'angular'; -import { downgradeInjectable } from '@angular/upgrade/static'; +import { downgradeComponent, downgradeInjectable } from '@angular/upgrade/static'; import { LabelService } from './labelService'; -import LabelAuthoring from './labelAuthoring'; import { EditLabelAdvancedComponent } from './edit-label-advanced/edit-label-advanced.component'; +import { LabelAuthoring } from './label-authoring/label-authoring.component'; const labelAuthoringComponentModule = angular .module('labelAuthoringComponentModule', ['pascalprecht.translate']) .service('LabelService', downgradeInjectable(LabelService)) - .component('labelAuthoring', LabelAuthoring) + .directive( + 'labelAuthoring', + downgradeComponent({ component: LabelAuthoring }) as angular.IDirectiveFactory + ) .component('editLabelAdvanced', EditLabelAdvancedComponent) .config([ '$translatePartialLoaderProvider', diff --git a/src/main/webapp/wise5/components/table/authoring.html b/src/main/webapp/wise5/components/table/authoring.html deleted file mode 100644 index 08bbdb80d3..0000000000 --- a/src/main/webapp/wise5/components/table/authoring.html +++ /dev/null @@ -1,381 +0,0 @@ -<div style='display: flex'> - <style> - .rotate90 { - -webkit-transform: rotate(90deg); - transform: rotate(90deg); - } - .rotate270 { - -webkit-transform: rotate(270deg); - transform: rotate(270deg); - } - </style> - <div style='flex: 1'> - <div> - <div class='advancedAuthoringDiv' - ng-if='tableController.showAdvancedAuthoring'> - <div> - <md-checkbox class='md-primary' - ng-model='tableController.authoringComponentContent.showSaveButton' - ng-change='tableController.componentChanged()'> - {{ ::'SHOW_SAVE_BUTTON' | translate }} - </md-checkbox> - </div> - <div> - <md-checkbox class='md-primary' - ng-model='tableController.authoringComponentContent.showSubmitButton' - ng-change='tableController.componentChanged()'> - {{ ::'SHOW_SUBMIT_BUTTON' | translate }} - </md-checkbox> - </div> - <div> - <md-checkbox class='md-primary' - ng-if='tableController.isNotebookEnabled()' - ng-model='tableController.authoringComponentContent.showAddToNotebookButton' - ng-change='tableController.componentChanged()'> - {{ ::'SHOW_ADD_TO_NOTEBOOK_BUTTON' | translate }} - </md-checkbox> - </div> - <div> - <md-input-container style='margin-right: 20px; width: 150px; height: 25px;' - ng-if='tableController.authoringComponentContent.showSubmitButton'> - <label>{{ ::'MAX_SUBMIT' | translate }}</label> - <input type='number' - ng-model='tableController.authoringComponentContent.maxSubmitCount' - ng-model-options='{ debounce: 1000 }' - ng-change='tableController.componentChanged()'/> - </md-input-container> - </div> - <div style='border: 1px solid black'> - <p>{{ ::'table.dataExplorer' | translate }}</p> - <div> - <md-checkbox class='md-primary' - ng-model='tableController.authoringComponentContent.isDataExplorerEnabled' - ng-change='tableController.toggleDataExplorer()'> - {{ ::'table.enableDataExplorer' | translate }} - </md-checkbox> - </div> - <div ng-if='tableController.authoringComponentContent.isDataExplorerEnabled'> - <div> - <p>{{ ::'table.allowedGraphTypes' | translate }}</p> - <md-checkbox class='md-primary' - style='margin-right: 40px;' - ng-model='tableController.isDataExplorerScatterPlotEnabled' - ng-change='tableController.dataExplorerToggleScatterPlot()'> - {{ ::'table.scatterPlot' | translate }} - </md-checkbox> - <md-checkbox class='md-primary' - style='margin-right: 40px;' - ng-model='tableController.isDataExplorerLineGraphEnabled' - ng-change='tableController.dataExplorerToggleLineGraph()'> - {{ ::'table.lineGraph' | translate }} - </md-checkbox> - <md-checkbox class='md-primary' - ng-model='tableController.isDataExplorerBarGraphEnabled' - ng-change='tableController.dataExplorerToggleBarGraph()'> - {{ ::'table.barGraph' | translate }} - </md-checkbox> - </div> - <div ng-if='tableController.isDataExplorerScatterPlotEnabled'> - <md-checkbox class='md-primary' - ng-model='tableController.authoringComponentContent.isDataExplorerScatterPlotRegressionLineEnabled' - ng-change='tableController.componentChanged()'> - {{ ::'table.showScatterPlotRegressionLine' | translate }} - </md-checkbox> - </div> - <div> - <md-input-container style='margin-right: 20px; width: 150px; height: 25px;'> - <label>{{ ::'table.numberOfSeries' | translate }}</label> - <input type='number' - ng-model='tableController.authoringComponentContent.numDataExplorerSeries' - ng-model-options='{ debounce: 1000 }' - ng-change='tableController.numDataExplorerSeriesChanged()'/> - </md-input-container> - </div> - <div> - <md-input-container style='margin-right: 20px; width: 150px; height: 25px;'> - <label>{{ ::'table.numberOfYAxes' | translate }}</label> - <input type='number' - ng-model='tableController.authoringComponentContent.numDataExplorerYAxis' - ng-model-options='{ debounce: 1000 }' - ng-change='tableController.numDataExplorerYAxisChanged()'/> - </md-input-container> - </div> - <div ng-if='tableController.authoringComponentContent.numDataExplorerYAxis > 1'> - <div ng-repeat='s in [].constructor(tableController.authoringComponentContent.numDataExplorerSeries) track by $index'> - <md-input-container style='margin-right: 20px; width: 300px;'> - <label>{{ ::'table.series' | translate }} {{ $index + 1 }}</label> - <md-select ng-model='tableController.authoringComponentContent.dataExplorerSeriesParams[$index].yAxis' - ng-change='tableController.componentChanged()' - style='width: 300px'> - <md-option ng-repeat='y in [].constructor(tableController.authoringComponentContent.numDataExplorerYAxis) track by $index' - ng-value='$index'> - <span> - {{ ::'table.yAxis' | translate }} {{ $index + 1 }} - </span> - </md-option> - </md-select> - </md-input-container> - </div> - </div> - <div> - <md-checkbox class='md-primary' - style='margin-right: 40px;' - ng-model='tableController.authoringComponentContent.isDataExplorerAxisLabelsEditable' - ng-change='tableController.componentChanged()'> - {{ ::'table.canStudentEditAxisLabels' | translate }} - </md-checkbox> - </div> - </div> - </div> - <div layout="column" layout-align="start start"> - <edit-component-max-score [authoring-component-content]="tableController.authoringComponentContent"></edit-component-max-score> - <edit-component-width [authoring-component-content]="tableController.authoringComponentContent"></edit-component-width> - <edit-component-rubric [authoring-component-content]="tableController.authoringComponentContent"></edit-component-rubric> - <edit-component-tags [authoring-component-content]="tableController.authoringComponentContent"></edit-component-tags> - </div> - <div> - <div style='height: 50;'> - <label class='node__label--vertical-alignment'> - {{ ::'CONNECTED_COMPONENTS' | translate }} - </label> - <md-button class='topButton md-raised md-primary' - ng-click='tableController.addConnectedComponent()'> - <md-icon>add</md-icon> - <md-tooltip md-direction='top' - class='projectButtonTooltip'> - {{ ::'ADD_CONNECTED_COMPONENT' | translate }} - </md-tooltip> - </md-button> - </div> - <div ng-repeat='connectedComponent in tableController.authoringComponentContent.connectedComponents track by $index' - style='border: 2px solid #dddddd; border-radius: 5px; margin-bottom: 10px; padding: 20px 20px 10px 20px;'> - <div flex> - <md-input-container style='margin-right: 20px; width: 300px;'> - <label>{{ ::'step' | translate }}</label> - <md-select ng-model='connectedComponent.nodeId' - ng-change='tableController.connectedComponentNodeIdChanged(connectedComponent)' - style='width: 300px'> - <md-option ng-repeat='item in tableController.idToOrder | toArray | orderBy : "order"' - value='{{item.$key}}' - ng-if='tableController.isApplicationNode(item.$key)'> - {{tableController.getNodePositionAndTitleByNodeId(item.$key)}} - </md-option> - </md-select> - </md-input-container> - <md-input-container style='margin-right: 20px; width: 300px;'> - <label>{{ ::'component' | translate }}</label> - <md-select ng-model='connectedComponent.componentId' - ng-change='tableController.connectedComponentComponentIdChanged(connectedComponent)' - style='width: 300px'> - <md-option ng-repeat='(componentIndex, component) in tableController.getComponentsByNodeId(connectedComponent.nodeId)' - value='{{component.id}}' - ng-disabled='!tableController.isConnectedComponentTypeAllowed(component.type) || component.id == tableController.componentId'> - {{ componentIndex + 1 }}. {{component.type}} - <span ng-if='component.id == tableController.componentId'> - ({{ ::'thisComponent' | translate }}) - </span> - </md-option> - </md-select> - </md-input-container> - <md-input-container style='margin-right: 20px; width: 200px;'> - <label>{{ ::'type' | translate }}</label> - <md-select ng-model='connectedComponent.type' - ng-change='tableController.connectedComponentTypeChanged(connectedComponent)' - style='width: 200px'> - <md-option value='importWork'> - {{ ::'importWork' | translate }} - </md-option> - <md-option value='showWork'> - {{ ::'showWork' | translate }} - </md-option> - </md-select> - </md-input-container> - <md-input-container style='margin-right: 20px; width: 200px;' - ng-if='connectedComponent.type == "importWork"'> - <label>{{ ::'action' | translate }}</label> - <md-select ng-model='connectedComponent.action' - ng-change='tableController.componentChanged()' - style='width: 200px'> - <md-option value='merge'> - {{ ::'merge' | translate }} - </md-option> - <md-option value='append'> - {{ ::'append' | translate }} - </md-option> - </md-select> - </md-input-container> - <span flex></span> - <md-input-container style='margin-left: 20px;'> - <md-button class='topButton md-raised md-primary' - ng-click='tableController.deleteConnectedComponent($index)'> - <md-icon>delete</md-icon> - <md-tooltip md-direction='top' - class='projectButtonTooltip'> - {{ ::'DELETE' | translate }} - </md-tooltip> - </md-button> - </md-input-container> - <md-input-container ng-if='tableController.getConnectedComponentType(connectedComponent) == "Graph"'> - <md-checkbox class='md-primary' - ng-model='connectedComponent.showDataAtMouseX' - ng-change='tableController.componentChanged()'> - {{ ::'table.onlyShowDataAtMouseXPosition' | translate }} - </md-checkbox> - </md-input-container> - </div> - </div> - </div> - <edit-component-json [node-id]="tableController.nodeId" [component-id]="tableController.componentId"></edit-component-json> - </div> - <br/> - <div ng-if='!tableController.authoringComponentContent.showPreviousWork'> - <md-input-container style='width:100%'> - <label>{{ ::'PROMPT' | translate }}</label> - <textarea rows='1' - ng-model='tableController.authoringComponentContent.prompt' - ng-change='tableController.componentChanged()' - ng-model-options='{ debounce: 1000 }' - aria-label='Prompt' - placeholder='{{ "enterPromptHere" | translate }}'> - </textarea> - </md-input-container> - <br/> - <div style='height: 60px;'> - <md-input-container> - <label>{{ ::'table.columns' | translate }}</label> - <input type='number' - style='width: 120px' - ng-model='tableController.authoringComponentContent.numColumns' - ng-model-options='{ debounce: 500 }' - ng-change='tableController.tableNumColumnsChanged({{tableController.authoringComponentContent.numColumns}})'/> - </md-input-container> - <md-input-container> - <label>{{ ::'table.rows' | translate }}</label> - <input type='number' - style='width: 120px' - ng-model-options='{ debounce: 500 }' - ng-model='tableController.authoringComponentContent.numRows' - ng-change='tableController.tableNumRowsChanged({{tableController.authoringComponentContent.numRows}})'/> - </md-input-container> - <md-input-container> - <label>{{ ::'table.globalCellSize' | translate }}</label> - <input type='number' - style='width: 120px' - ng-model='tableController.authoringComponentContent.globalCellSize' - ng-change='tableController.componentChanged()'/> - </md-input-container> - </div> - <br/> - <table style='border: 1px solid black;'> - <tr> - <td style='border: 1px solid black;'></td> - <td ng-repeat='x in [].constructor(tableController.authoringComponentContent.numColumns) track by $index' - style='border: 1px solid black;'> - <md-button class='topButton md-raised md-primary' - ng-click='tableController.insertColumn($index)'> - <md-icon class='rotate90'>loupe</md-icon> - <md-tooltip md-direction='top' - class='projectButtonTooltip'> - {{ ::'table.insertColumnBefore' | translate }} - </md-tooltip> - </md-button> - <md-button class='topButton md-raised md-primary' - ng-click='tableController.deleteColumn($index)'> - <md-icon>delete</md-icon> - <md-tooltip md-direction='top' - class='projectButtonTooltip'> - {{ ::'table.deleteColumn' | translate }} - </md-tooltip> - </md-button> - <md-button class='topButton md-raised md-primary' - ng-if='$last' - ng-click='tableController.insertColumn($index + 1)'> - <md-icon>loupe</md-icon> - <md-tooltip md-direction='top' - class='projectButtonTooltip'> - {{ ::'table.insertColumnAfter' | translate }} - </md-tooltip> - </md-button> - </td> - </tr> - <tr ng-repeat='row in tableController.authoringComponentContent.tableData'> - <td style='border: 1px solid black;'> - <md-button class='topButton md-raised md-primary' - ng-click='tableController.insertRow($index)'> - <md-icon class='rotate270'>loupe</md-icon> - <md-tooltip md-direction='top' - class='projectButtonTooltip'> - {{ ::'table.insertRowBefore' | translate }} - </md-tooltip> - </md-button> - <br/> - <md-button class='topButton md-raised md-primary' - ng-click='tableController.deleteRow($index)'> - <md-icon>delete</md-icon> - <md-tooltip md-direction='top' - class='projectButtonTooltip'> - {{ ::'table.deleteRow' | translate }} - </md-tooltip> - </md-button> - <br/> - <md-button class='topButton md-raised md-primary' - ng-if='$last' - ng-click='tableController.insertRow($index + 1)'> - <md-icon>loupe</md-icon> - <md-tooltip md-direction='top' - class='projectButtonTooltip'> - {{ ::'table.insertRowAfter' | translate }} - </md-tooltip> - </md-button> - </td> - <td ng-repeat='cell in row' style='border: 1px solid black; height: 20;'> - <md-input-container style='height: 20px;'> - <input ng-model='cell.text' - ng-model-options='{ debounce: 1000 }' - ng-change='tableController.componentChanged()' - aria-label='Text' - size='20'/> - </md-input-container> - <br/> - <md-checkbox class='md-primary' - style='margin-left: 10px;' - ng-model='cell.editable' - ng-change='tableController.componentChanged()'> - {{ ::'table.editableByStudent' | translate }} - </md-checkbox> - </td> - </tr> - <tr> - <td></td> - <td ng-repeat='x in [].constructor(tableController.authoringComponentContent.numColumns) track by $index'> -   - </td> - </tr> - <tr> - <td style='border: 1px solid black; height: 20px;'> - <label style='padding: 5px;'>{{ ::'OPTIONAL' | translate }}</label> - </td> - <td ng-repeat='x in [].constructor(tableController.authoringComponentContent.numColumns) track by $index' style='border: 1px solid black; height: 20px;'> - <md-input-container style='height: 20px;'> - <label>{{ ::'table.columnCellSizes' | translate }}</label> - <input type='number' - ng-model='tableController.columnCellSizes[$index]' - ng-change='tableController.columnSizeChanged($index)'/> - </md-input-container> - </td> - </tr> - </table> - <br/> - <div layout='row'> - <md-button class='topButton md-raised md-primary' - ng-click='tableController.makeAllCellsEditable()'> - {{ ::'table.makeAllCellsEditable' | translate }} - </md-button> - <md-button class='topButton md-raised md-primary' - ng-click='tableController.makeAllCellsUneditable()'> - {{ ::'table.makeAllCellsUneditable' | translate }} - </md-button> - </div> - </div> - </div> - </div> diff --git a/src/main/webapp/wise5/components/table/table-authoring/table-authoring.component.html b/src/main/webapp/wise5/components/table/table-authoring/table-authoring.component.html new file mode 100644 index 0000000000..9fcbfba3af --- /dev/null +++ b/src/main/webapp/wise5/components/table/table-authoring/table-authoring.component.html @@ -0,0 +1,181 @@ +<mat-form-field class="prompt"> + <mat-label i18n>Prompt</mat-label> + <textarea matInput + [ngModel]="authoringComponentContent.prompt" + (ngModelChange)="promptChanged($event)" + placeholder="Enter Prompt Here" + i18n-placeholder + cdkTextareaAutosize> + </textarea> +</mat-form-field> +<div> + <mat-form-field class="size-input"> + <mat-label i18n>Columns</mat-label> + <input matInput + type="number" + min="1" + [(ngModel)]="authoringComponentContent.numColumns" + (ngModelChange)="numColumnsChange.next($event)"> + </mat-form-field> + <mat-form-field class="size-input"> + <mat-label i18n>Rows</mat-label> + <input matInput + type="number" + min="1" + [(ngModel)]="authoringComponentContent.numRows" + (ngModelChange)="numRowsChange.next($event)"/> + </mat-form-field> + <mat-form-field class="size-input"> + <mat-label i18n>Global Cell Size</mat-label> + <input matInput + type="number" + min="1" + [(ngModel)]="authoringComponentContent.globalCellSize" + (ngModelChange)="globalCellSizeChange.next($event)"/> + </mat-form-field> +</div> +<table class="table"> + <tr> + <td></td> + <td *ngFor="let x of [].constructor(getNumColumnsInTableData()); index as columnIndex; last as isLast" + class="outer-cell-container"> + <div fxLayout="row wrap" fxLayoutAlign="space-between center"> + <button mat-raised-button + color="primary" + class="column-buttons" + (click)="insertColumn(columnIndex)" + matTooltip="Insert Column Before" + matTooltipPosition="above" + i18n-matTooltip + aria-label="Insert Column Before" + i18n-aria-label> + <mat-icon class="rotate90">loupe</mat-icon> + </button> + <button mat-raised-button + color="primary" + class="column-buttons" + (click)="deleteColumn(columnIndex)" + matTooltip="Delete Column" + matTooltipPosition="above" + i18n-matTooltip + aria-label="Delete" + i18n-aria-label> + <mat-icon>delete</mat-icon> + </button> + <button mat-raised-button + color="primary" + class="column-buttons" + *ngIf="isLast" + (click)="insertColumn(columnIndex + 1)" + matTooltip="Insert Column After" + matTooltipPosition="above" + i18n-matTooltip + aria-label="Insert Column After" + i18n-aria-label> + <mat-icon>loupe</mat-icon> + </button> + <div *ngIf="!isLast" class="spacer"> + </div> + </div> + </td> + </tr> + <tr *ngFor="let row of authoringComponentContent.tableData; index as rowIndex; last as isLast"> + <td class="outer-cell-container" fxLayout="column" fxLayoutAlign="center center"> + <button mat-raised-button + color="primary" + class="row-buttons" + (click)="insertRow(rowIndex)" + matTooltip="Insert Row Before" + matTooltipPosition="above" + i18n-matTooltip + aria-label="Insert Row Before" + i18n-aria-label> + <mat-icon class="rotate270">loupe</mat-icon> + </button> + <button mat-raised-button + color="primary" + class="row-buttons" + (click)="deleteRow(rowIndex)" + matTooltip="Delete Row" + matTooltipPosition="above" + i18n-matTooltip + aria-label="Delete" + i18n-aria-label> + <mat-icon>delete</mat-icon> + </button> + <button mat-raised-button + color="primary" + class="row-buttons" + *ngIf="isLast" + (click)="insertRow(rowIndex + 1)" + matTooltip="Insert Row After" + matTooltipPosition="above" + i18n-matTooltip + aria-label="Insert Row After" + i18n-aria-label> + <mat-icon>loupe</mat-icon> + </button> + </td> + <td *ngFor="let cell of row" + class="inner-cell-container"> + <div fxLayout="column" fxLayoutAlign="center center"> + <mat-form-field class="cell-text"> + <input matInput + [(ngModel)]="cell.text" + (ngModelChange)="inputChange.next($event)" + ng-model-options="{ debounce: 1000 }" + aria-label="Text"/> + </mat-form-field> + <mat-checkbox + color="primary" + [(ngModel)]="cell.editable" + (ngModelChange)="componentChanged()" + i18n> + Editable + </mat-checkbox> + </div> + </td> + </tr> + <tr> + <td class="blank-row-cell"></td> + <td *ngFor="let x of [].constructor(getNumColumnsInTableData())" + class="blank-row-cell"> + </td> + </tr> + <tr> + <td class="outer-cell-container"> + <div fxLayoutAlign="center center"> + Optional + </div> + </td> + <td *ngFor="let x of [].constructor(getNumColumnsInTableData()); index as cellIndex" + class="inner-cell-container"> + <div fxLayoutAlign="center center"> + <mat-form-field> + <mat-label i18n>Column Cell Size</mat-label> + <input matInput + type="number" + min="1" + [(ngModel)]="columnCellSizes[cellIndex]" + (ngModelChange)="columnSizeChanged(cellIndex)"/> + </mat-form-field> + </div> + </td> + </tr> +</table> +<div fxLayout="row"> + <button mat-raised-button + color="primary" + class="make-all-cells-editable-buttons" + (click)="setAllCellsEditable()" + i18n> + Make All Cells Editable + </button> + <button mat-raised-button + color="primary" + class="make-all-cells-editable-buttons" + (click)="setAllCellsUneditable()" + i18n> + Make All Cells Uneditable + </button> +</div> \ No newline at end of file diff --git a/src/main/webapp/wise5/components/table/table-authoring/table-authoring.component.scss b/src/main/webapp/wise5/components/table/table-authoring/table-authoring.component.scss new file mode 100644 index 0000000000..cdd97d67ff --- /dev/null +++ b/src/main/webapp/wise5/components/table/table-authoring/table-authoring.component.scss @@ -0,0 +1,62 @@ +.prompt { + width: 100%; +} + +.size-input { + width: 120px; + margin-right: 20px; +} + +.table { + width: 100%; +} + +.rotate90 { + -webkit-transform: rotate(90deg); + transform: rotate(90deg); +} + +.rotate270 { + -webkit-transform: rotate(270deg); + transform: rotate(270deg); +} + +.column-buttons { + margin: 5px; +} + +.row-buttons { + margin-top: 5px; + margin-bottom: 5px; +} + +.spacer { + width: 56px; +} + +.outer-cell-container { + border: 1px solid black; + padding: 5px; + width: auto; +} + +.inner-cell-container { + border: 1px solid black; + padding: 10px; +} + +.cell-text { + width: 100%; +} + +.blank-row-cell { + height: 20px; +} + +::ng-deep .mat-form-field-infix { + width: auto !important; +} + +.make-all-cells-editable-buttons { + margin: 10px; +} \ No newline at end of file diff --git a/src/main/webapp/wise5/components/table/table-authoring/table-authoring.component.ts b/src/main/webapp/wise5/components/table/table-authoring/table-authoring.component.ts new file mode 100644 index 0000000000..0d4cfa8f31 --- /dev/null +++ b/src/main/webapp/wise5/components/table/table-authoring/table-authoring.component.ts @@ -0,0 +1,361 @@ +'use strict'; + +import { Component } from '@angular/core'; +import { Subject, Subscription } from 'rxjs'; +import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; +import { ProjectAssetService } from '../../../../site/src/app/services/projectAssetService'; +import { ComponentAuthoring } from '../../../authoringTool/components/component-authoring.component'; +import { ConfigService } from '../../../services/configService'; +import { NodeService } from '../../../services/nodeService'; +import { TeacherProjectService } from '../../../services/teacherProjectService'; + +@Component({ + selector: 'table-authoring', + templateUrl: 'table-authoring.component.html', + styleUrls: ['table-authoring.component.scss'] +}) +export class TableAuthoring extends ComponentAuthoring { + columnCellSizes: any; + + numColumnsChange: Subject<number> = new Subject<number>(); + numRowsChange: Subject<number> = new Subject<number>(); + globalCellSizeChange: Subject<number> = new Subject<number>(); + inputChange: Subject<string> = new Subject<string>(); + + numColumnsChangeSubscription: Subscription; + numRowsChangeSubscription: Subscription; + globalCellSizeChangeSubscription: Subscription; + inputChangeSubscription: Subscription; + + constructor( + protected ConfigService: ConfigService, + protected NodeService: NodeService, + protected ProjectAssetService: ProjectAssetService, + protected ProjectService: TeacherProjectService + ) { + super(ConfigService, NodeService, ProjectAssetService, ProjectService); + this.numColumnsChangeSubscription = this.numColumnsChange + .pipe(debounceTime(1000), distinctUntilChanged()) + .subscribe(() => { + this.tableNumColumnsChanged(); + }); + this.numRowsChangeSubscription = this.numRowsChange + .pipe(debounceTime(1000), distinctUntilChanged()) + .subscribe(() => { + this.tableNumRowsChanged(); + }); + this.globalCellSizeChangeSubscription = this.globalCellSizeChange + .pipe(debounceTime(1000), distinctUntilChanged()) + .subscribe(() => { + this.componentChanged(); + }); + this.inputChangeSubscription = this.inputChange + .pipe(debounceTime(1000), distinctUntilChanged()) + .subscribe(() => { + this.componentChanged(); + }); + } + + ngOnInit() { + super.ngOnInit(); + this.columnCellSizes = this.parseColumnCellSizes(this.componentContent); + } + + ngOnDestroy() { + super.ngOnDestroy(); + this.unsubscribeAll(); + } + + unsubscribeAll() { + this.numColumnsChangeSubscription.unsubscribe(); + this.numRowsChangeSubscription.unsubscribe(); + this.globalCellSizeChangeSubscription.unsubscribe(); + this.inputChangeSubscription.unsubscribe(); + } + + tableNumRowsChanged(): void { + const oldValue = this.getNumRowsInTableData(); + const newValue = this.authoringComponentContent.numRows; + if (newValue < oldValue) { + if (this.areRowsAfterEmpty(newValue)) { + this.tableSizeChanged(); + } else { + if (confirm($localize`Are you sure you want to decrease the number of rows?`)) { + this.tableSizeChanged(); + } else { + this.authoringComponentContent.numRows = oldValue; + } + } + } else { + this.tableSizeChanged(); + } + } + + areRowsAfterEmpty(rowIndex: number): boolean { + const oldNumRows = this.getNumRowsInTableData(); + for (let r = rowIndex; r < oldNumRows; r++) { + if (!this.isRowEmpty(r)) { + return false; + } + } + return true; + } + + isRowEmpty(rowIndex: number): boolean { + const tableData = this.authoringComponentContent.tableData; + for (const cell of tableData[rowIndex]) { + if (!this.isEmpty(cell.text)) { + return false; + } + } + return true; + } + + tableNumColumnsChanged(): void { + const oldValue = this.getNumColumnsInTableData(); + const newValue = this.authoringComponentContent.numColumns; + if (newValue < oldValue) { + if (this.areColumnsAfterEmpty(newValue)) { + this.tableSizeChanged(); + } else { + if (confirm($localize`Are you sure you want to decrease the number of columns?`)) { + this.tableSizeChanged(); + } else { + this.authoringComponentContent.numColumns = oldValue; + } + } + } else { + this.tableSizeChanged(); + } + } + + areColumnsAfterEmpty(columnIndex: number): boolean { + const oldNumColumns = this.getNumColumnsInTableData(); + for (let c = columnIndex; c < oldNumColumns; c++) { + if (!this.isColumnEmpty(c)) { + return false; + } + } + return true; + } + + isColumnEmpty(columnIndex: number): boolean { + for (const row of this.authoringComponentContent.tableData) { + const cell = row[columnIndex]; + if (!this.isEmpty(cell.text)) { + return false; + } + } + return true; + } + + isEmpty(txt: string): boolean { + return txt == null || txt == ''; + } + + tableSizeChanged(): void { + this.authoringComponentContent.tableData = this.getUpdatedTable( + this.authoringComponentContent.numRows, + this.authoringComponentContent.numColumns + ); + this.componentChanged(); + } + + /** + * Create a table with the given dimensions. Populate the cells with the cells from the old table. + * @param newNumRows the number of rows in the new table + * @param newNumColumns the number of columns in the new table + * @returns a new table + */ + getUpdatedTable(newNumRows: number, newNumColumns: number): any { + const newTable = []; + for (let r = 0; r < newNumRows; r++) { + const newRow = []; + for (let c = 0; c < newNumColumns; c++) { + let cell = this.getCellObjectFromTableData(c, r); + if (cell == null) { + cell = this.createEmptyCell(); + } + newRow.push(cell); + } + newTable.push(newRow); + } + return newTable; + } + + /** + * Get the cell object at the given x, y location + * @param x the column number (zero indexed) + * @param y the row number (zero indexed) + * @returns the cell at the given x, y location or null if there is none + */ + getCellObjectFromTableData(x: number, y: number): any { + let cellObject = null; + const tableData = this.authoringComponentContent.tableData; + if (tableData != null) { + const row = tableData[y]; + if (row != null) { + cellObject = row[x]; + } + } + return cellObject; + } + + createEmptyCell(): any { + return { + text: '', + editable: true, + size: null + }; + } + + insertRow(rowIndex: number): void { + const tableData = this.authoringComponentContent.tableData; + const newRow = []; + const numColumns = this.authoringComponentContent.numColumns; + for (let c = 0; c < numColumns; c++) { + const newCell = this.createEmptyCell(); + const cellSize = this.columnCellSizes[c]; + if (cellSize != null) { + newCell.size = cellSize; + } + newRow.push(newCell); + } + tableData.splice(rowIndex, 0, newRow); + this.authoringComponentContent.numRows++; + this.componentChanged(); + } + + deleteRow(rowIndex: number): void { + if (confirm($localize`Are you sure you want to delete this row?`)) { + const tableData = this.authoringComponentContent.tableData; + if (tableData != null) { + tableData.splice(rowIndex, 1); + this.authoringComponentContent.numRows--; + } + this.componentChanged(); + } + } + + insertColumn(columnIndex: number): void { + const tableData = this.authoringComponentContent.tableData; + const numRows = this.authoringComponentContent.numRows; + for (let r = 0; r < numRows; r++) { + const row = tableData[r]; + const newCell = this.createEmptyCell(); + row.splice(columnIndex, 0, newCell); + } + this.authoringComponentContent.numColumns++; + this.parseColumnCellSizes(this.authoringComponentContent); + this.componentChanged(); + } + + deleteColumn(columnIndex: number): void { + if (confirm($localize`Are you sure you want to delete this column?`)) { + const tableData = this.authoringComponentContent.tableData; + const numRows = this.authoringComponentContent.numRows; + for (let r = 0; r < numRows; r++) { + const row = tableData[r]; + row.splice(columnIndex, 1); + } + this.authoringComponentContent.numColumns--; + this.parseColumnCellSizes(this.authoringComponentContent); + this.componentChanged(); + } + } + + /** + * Get the number of rows in the table data. This is slightly different from just getting the + * numRows field in the component content. Usually the number of rows will be the same. In some + * cases it can be different such as during authoring immediately after the author changes the + * number of rows using the number of rows input. + * @return {number} The number of rows in the table data. + */ + getNumRowsInTableData(): number { + return this.authoringComponentContent.tableData.length; + } + + /** + * Get the number of columns in the table data. This is slightly different from just getting the + * numColumns field in the component content. Usually the number of columns will be the same. In + * some cases it can be different such as during authoring immediately after the author changes + * the number of columns using the number of columns input. + * @return {number} The number of columns in the table data. + */ + getNumColumnsInTableData(): number { + const tableData = this.authoringComponentContent.tableData; + if (tableData.length > 0) { + return tableData[0].length; + } + return 0; + } + + setAllCellsUneditable(): void { + this.setAllCellsIsEditable(false); + this.componentChanged(); + } + + setAllCellsEditable(): void { + this.setAllCellsIsEditable(true); + this.componentChanged(); + } + + setAllCellsIsEditable(isEditable: boolean): void { + for (const row of this.authoringComponentContent.tableData) { + for (const cell of row) { + cell.editable = isEditable; + } + } + } + + /** + * Parse the column cell sizes. We will get the column cell sizes by looking at the size value of + * each cell in the first row. + * @param componentContent the component content + */ + parseColumnCellSizes(componentContent: any): any { + const columnCellSizes = {}; + const tableData = componentContent.tableData; + const firstRow = tableData[0]; + if (firstRow != null) { + for (let x = 0; x < firstRow.length; x++) { + const cell = firstRow[x]; + columnCellSizes[x] = cell.size; + } + } + return columnCellSizes; + } + + columnSizeChanged(index: number): void { + let cellSize = this.columnCellSizes[index]; + if (cellSize == '') { + cellSize = null; + } + this.setColumnCellSizes(index, cellSize); + } + + setColumnCellSizes(column: number, size: number): void { + const tableData = this.authoringComponentContent.tableData; + for (let r = 0; r < tableData.length; r++) { + const row = tableData[r]; + const cell = row[column]; + if (cell != null) { + cell.size = size; + } + } + this.componentChanged(); + } + + automaticallySetConnectedComponentFieldsIfPossible(connectedComponent) { + if (connectedComponent.type === 'importWork' && connectedComponent.action == null) { + connectedComponent.action = 'merge'; + } else if (connectedComponent.type === 'showWork') { + connectedComponent.action = null; + } + } + + connectedComponentTypeChanged(connectedComponent) { + this.automaticallySetConnectedComponentFieldsIfPossible(connectedComponent); + this.componentChanged(); + } +} diff --git a/src/main/webapp/wise5/components/table/tableAuthoring.ts b/src/main/webapp/wise5/components/table/tableAuthoring.ts deleted file mode 100644 index beef4e7fef..0000000000 --- a/src/main/webapp/wise5/components/table/tableAuthoring.ts +++ /dev/null @@ -1,428 +0,0 @@ -'use strict'; - -import { Directive } from '@angular/core'; -import { EditComponentController } from '../../authoringTool/components/editComponentController'; - -@Directive() -class TableAuthoringController extends EditComponentController { - columnCellSizes: any; - - static $inject = [ - '$filter', - 'ConfigService', - 'NodeService', - 'NotificationService', - 'ProjectAssetService', - 'ProjectService', - 'UtilService' - ]; - - constructor( - $filter, - ConfigService, - NodeService, - NotificationService, - ProjectAssetService, - ProjectService, - UtilService - ) { - super( - $filter, - ConfigService, - NodeService, - NotificationService, - ProjectAssetService, - ProjectService, - UtilService - ); - } - - $onInit() { - super.$onInit(); - this.columnCellSizes = this.parseColumnCellSizes(this.componentContent); - } - - tableNumRowsChanged(oldValue: number): void { - if (this.authoringComponentContent.numRows < oldValue) { - if (this.areRowsAfterEmpty(this.authoringComponentContent.numRows)) { - this.tableSizeChanged(); - } else { - if (confirm(this.$translate('table.areYouSureYouWantToDecreaseTheNumberOfRows'))) { - this.tableSizeChanged(); - } else { - this.authoringComponentContent.numRows = oldValue; - } - } - } else { - this.tableSizeChanged(); - } - } - - /** - * Determine if the rows after the given index are empty. - * @param rowIndex The index of the row to start checking at. This value is zero indexed. - * @return {boolean} True if the row at the given index and all the rows after are empty. - * False if the row at the given index or any row after the row index is not empty. - */ - areRowsAfterEmpty(rowIndex: number): boolean { - const oldNumRows = this.getNumRowsInTableData(); - for (let r = rowIndex; r < oldNumRows; r++) { - if (!this.isRowEmpty(r)) { - return false; - } - } - return true; - } - - /** - * Determine if a row has cells that are all empty string. - * @param rowIndex The row index. This value is zero indexed. - * @returns {boolean} True if the text in all the cells in the row are empty string. - * False if the text in any cell in the row is not empty string. - */ - isRowEmpty(rowIndex: number): boolean { - const tableData = this.authoringComponentContent.tableData; - for (const cell of tableData[rowIndex]) { - if (cell.text != null && cell.text != '') { - return false; - } - } - return true; - } - - /** - * The author has changed the number of columns. - * @param oldValue The previous number of columns. - */ - tableNumColumnsChanged(oldValue: number): void { - if (this.authoringComponentContent.numColumns < oldValue) { - // the author is reducing the number of columns - if (this.areColumnsAfterEmpty(this.authoringComponentContent.numColumns)) { - // the columns that we will delete are empty so we will remove the columns - this.tableSizeChanged(); - } else { - if (confirm(this.$translate('table.areYouSureYouWantToDecreaseTheNumberOfColumns'))) { - this.tableSizeChanged(); - } else { - this.authoringComponentContent.numColumns = oldValue; - } - } - } else { - // the author is increasing the number of columns - this.tableSizeChanged(); - } - } - - /** - * Determine if the columns after the given index are empty. - * @param columnIndex The index of the column to start checking at. This value is zero indexed. - * @return {boolean} True if the column at the given index and all the columns after are empty. - * False if the column at the given index or any column after the column index is not empty. - */ - areColumnsAfterEmpty(columnIndex: number): boolean { - const oldNumColumns = this.getNumColumnsInTableData(); - for (let c = columnIndex; c < oldNumColumns; c++) { - if (!this.isColumnEmpty(c)) { - return false; - } - } - return true; - } - - /** - * Determine if a column has cells that are all empty string. - * @param columnIndex The column index. This value is zero indexed. - * @returns {boolean} True if the text in all the cells in the column are empty string. - * False if the text in any cell in the column is not empty string. - */ - isColumnEmpty(columnIndex: number): boolean { - for (const row of this.authoringComponentContent.tableData) { - const cell = row[columnIndex]; - if (cell.text != null && cell.text != '') { - return false; - } - } - return true; - } - - /** - * The table size has changed in the authoring view so we will update it - */ - tableSizeChanged(): void { - this.authoringComponentContent.tableData = this.getUpdatedTableSize( - this.authoringComponentContent.numRows, - this.authoringComponentContent.numColumns - ); - this.componentChanged(); - } - - /** - * Create a table with the given dimensions. Populate the cells with - * the cells from the old table. - * @param newNumRows the number of rows in the new table - * @param newNumColumns the number of columns in the new table - * @returns a new table - */ - getUpdatedTableSize(newNumRows: number, newNumColumns: number): any { - const newTable = []; - for (let r = 0; r < newNumRows; r++) { - const newRow = []; - for (let c = 0; c < newNumColumns; c++) { - let cell = this.getCellObjectFromComponentContent(c, r); - if (cell == null) { - cell = this.createEmptyCell(); - } - newRow.push(cell); - } - newTable.push(newRow); - } - return newTable; - } - - /** - * Get the cell object at the given x, y location - * @param x the column number (zero indexed) - * @param y the row number (zero indexed) - * @returns the cell at the given x, y location or null if there is none - */ - getCellObjectFromComponentContent(x: number, y: number): any { - let cellObject = null; - const tableData = this.authoringComponentContent.tableData; - if (tableData != null) { - const row = tableData[y]; - if (row != null) { - cellObject = row[x]; - } - } - return cellObject; - } - - createEmptyCell(): any { - return { - text: '', - editable: true, - size: null - }; - } - - /** - * Insert a row into the table from the authoring view - * @param y the row number to insert at - */ - insertRow(y: number): void { - const tableData = this.authoringComponentContent.tableData; - if (tableData != null) { - const newRow = []; - const numColumns = this.authoringComponentContent.numColumns; - for (let c = 0; c < numColumns; c++) { - const newCell = this.createEmptyCell(); - const cellSize = this.columnCellSizes[c]; - if (cellSize != null) { - newCell.size = cellSize; - } - newRow.push(newCell); - } - tableData.splice(y, 0, newRow); - this.authoringComponentContent.numRows++; - } - this.componentChanged(); - } - - /** - * Delete a row in the table from the authoring view - * @param y the row number to delete - */ - deleteRow(y: number): void { - if (confirm(this.$translate('table.areYouSureYouWantToDeleteThisRow'))) { - const tableData = this.authoringComponentContent.tableData; - if (tableData != null) { - tableData.splice(y, 1); - this.authoringComponentContent.numRows--; - } - this.componentChanged(); - } - } - - /** - * Insert a column into the table from the authoring view - * @param x the column number to insert at - */ - insertColumn(x: number): void { - const tableData = this.authoringComponentContent.tableData; - if (tableData != null) { - const numRows = this.authoringComponentContent.numRows; - for (let r = 0; r < numRows; r++) { - const tempRow = tableData[r]; - if (tempRow != null) { - const newCell = this.createEmptyCell(); - tempRow.splice(x, 0, newCell); - } - } - this.authoringComponentContent.numColumns++; - this.parseColumnCellSizes(this.authoringComponentContent); - } - this.componentChanged(); - } - - /** - * Delete a column in the table from the authoring view - * @param x the column number to delete - */ - deleteColumn(x: number): void { - if (confirm(this.$translate('table.areYouSureYouWantToDeleteThisColumn'))) { - const tableData = this.authoringComponentContent.tableData; - if (tableData != null) { - const numRows = this.authoringComponentContent.numRows; - for (let r = 0; r < numRows; r++) { - const tempRow = tableData[r]; - if (tempRow != null) { - tempRow.splice(x, 1); - } - } - this.authoringComponentContent.numColumns--; - this.parseColumnCellSizes(this.authoringComponentContent); - } - this.componentChanged(); - } - } - - /** - * Get the number of rows in the table data. This is slightly different from - * just getting the numRows field in the component content. Usually the - * number of rows will be the same. In some cases it can be different - * such as during authoring immediately after the author changes the number - * of rows using the number of rows input. - * @return {number} The number of rows in the table data. - */ - getNumRowsInTableData(): number { - return this.authoringComponentContent.tableData.length; - } - - /** - * Get the number of columns in the table data. This is slightly different from - * just getting the numColumns field in the component content. Usually the - * number of columns will be the same. In some cases it can be different - * such as during authoring immediately after the author changes the number - * of columns using the number of columns input. - * @return {number} The number of columns in the table data. - */ - getNumColumnsInTableData(): number { - const tableData = this.authoringComponentContent.tableData; - if (tableData.length > 0) { - return tableData[0].length; - } - return 0; - } - - makeAllCellsUneditable(): void { - const tableData = this.authoringComponentContent.tableData; - if (tableData != null) { - for (let r = 0; r < tableData.length; r++) { - const row = tableData[r]; - if (row != null) { - for (let c = 0; c < row.length; c++) { - const cell = row[c]; - if (cell != null) { - cell.editable = false; - } - } - } - } - } - this.componentChanged(); - } - - makeAllCellsEditable(): void { - const tableData = this.authoringComponentContent.tableData; - if (tableData != null) { - for (let r = 0; r < tableData.length; r++) { - const row = tableData[r]; - if (row != null) { - for (let c = 0; c < row.length; c++) { - const cell = row[c]; - if (cell != null) { - cell.editable = true; - } - } - } - } - } - this.componentChanged(); - } - - /** - * Parse the column cell sizes. We will get the column cell sizes by looking - * at size value of each column in the first row. - * @param componentContent the component content - */ - parseColumnCellSizes(componentContent: any): any { - const columnCellSizes = {}; - const tableData = componentContent.tableData; - if (tableData != null) { - const firstRow = tableData[0]; - if (firstRow != null) { - for (let x = 0; x < firstRow.length; x++) { - const cell = firstRow[x]; - columnCellSizes[x] = cell.size; - } - } - } - return columnCellSizes; - } - - columnSizeChanged(index: number): void { - if (index != null) { - let cellSize = this.columnCellSizes[index]; - if (cellSize == '') { - cellSize = null; - } - this.setColumnCellSizes(index, cellSize); - } - } - - /** - * Set the cell sizes for all the cells in a column - * @param column the column number - * @param size the cell size - */ - setColumnCellSizes(column: number, size: number): void { - const tableData = this.authoringComponentContent.tableData; - if (tableData != null) { - for (let r = 0; r < tableData.length; r++) { - const row = tableData[r]; - if (row != null) { - const cell = row[column]; - if (cell != null) { - cell.size = size; - } - } - } - } - this.componentChanged(); - } - - automaticallySetConnectedComponentFieldsIfPossible(connectedComponent) { - if (connectedComponent.type === 'importWork' && connectedComponent.action == null) { - connectedComponent.action = 'merge'; - } else if (connectedComponent.type === 'showWork') { - connectedComponent.action = null; - } - } - - connectedComponentTypeChanged(connectedComponent) { - this.automaticallySetConnectedComponentFieldsIfPossible(connectedComponent); - this.componentChanged(); - } -} - -const TableAuthoring = { - bindings: { - nodeId: '@', - componentId: '@' - }, - controller: TableAuthoringController, - controllerAs: 'tableController', - templateUrl: 'wise5/components/table/authoring.html' -}; - -export default TableAuthoring; diff --git a/src/main/webapp/wise5/components/table/tableAuthoringComponentModule.ts b/src/main/webapp/wise5/components/table/tableAuthoringComponentModule.ts index 60f9c96360..c202b806cd 100644 --- a/src/main/webapp/wise5/components/table/tableAuthoringComponentModule.ts +++ b/src/main/webapp/wise5/components/table/tableAuthoringComponentModule.ts @@ -2,14 +2,17 @@ import * as angular from 'angular'; import { TableService } from './tableService'; -import { downgradeInjectable } from '@angular/upgrade/static'; -import TableAuthoring from './tableAuthoring'; +import { downgradeComponent, downgradeInjectable } from '@angular/upgrade/static'; import { EditTableAdvancedComponent } from './edit-table-advanced/edit-table-advanced.component'; +import { TableAuthoring } from './table-authoring/table-authoring.component'; const tableAuthoringComponentModule = angular .module('tableAuthoringComponentModule', ['pascalprecht.translate']) .service('TableService', downgradeInjectable(TableService)) - .component('tableAuthoring', TableAuthoring) + .directive( + 'tableAuthoring', + downgradeComponent({ component: TableAuthoring }) as angular.IDirectiveFactory + ) .component('editTableAdvanced', EditTableAdvancedComponent) .config([ '$translatePartialLoaderProvider', diff --git a/src/main/webapp/wise5/services/milestoneService.ts b/src/main/webapp/wise5/services/milestoneService.ts index 47580d6e77..561e31ed79 100644 --- a/src/main/webapp/wise5/services/milestoneService.ts +++ b/src/main/webapp/wise5/services/milestoneService.ts @@ -9,6 +9,7 @@ import { TeacherDataService } from './teacherDataService'; import { UtilService } from './utilService'; import { Injectable } from '@angular/core'; import { UpgradeModule } from '@angular/upgrade/static'; +import { MilestoneDetailsDialog } from '../classroomMonitor/classroomMonitorComponents/milestones/milestoneDetailsDialog/milestoneDetailsDialog'; @Injectable() export class MilestoneService { @@ -511,40 +512,12 @@ export class MilestoneService { } showMilestoneDetails(milestone: any, $event: any, hideStudentWork: boolean = false) { - const title = this.getTranslation('MILESTONE_DETAILS_TITLE', { - name: milestone.name - }); - const template = `<md-dialog class="dialog--wider"> - <md-toolbar> - <div class="md-toolbar-tools"> - <h2>${title}</h2> - </div> - </md-toolbar> - <md-dialog-content class="gray-lighter-bg md-dialog-content"> - <milestone-details milestone="milestone" - hide-student-work="hideStudentWork" - on-show-workgroup="onShowWorkgroup(value)" - on-visit-node-grading="onVisitNodeGrading()"></milestone-details> - </md-dialog-content> - <md-dialog-actions layout="row" layout-align="start center"> - <span flex></span> - <md-button class="md-primary" - ng-click="edit()" - ng-if="milestone.type !== 'milestoneReport'" - aria-label="{{ ::'EDIT' | translate }}"> - {{ ::'EDIT' | translate }} - </md-button> - <md-button class="md-primary" - ng-click="close()" - aria-label="{{ ::'CLOSE' | translate }}"> - {{ ::'CLOSE' | translate }} - </md-button> - </md-dialog-actions> - </md-dialog>`; this.upgrade.$injector.get('$mdDialog').show({ parent: angular.element(document.body), - template: template, - ariaLabel: title, + templateUrl: + 'wise5/classroomMonitor/classroomMonitorComponents/milestones/milestoneDetailsDialog/milestoneDetailsDialog.html', + controller: MilestoneDetailsDialog, + controllerAs: '$ctrl', fullscreen: true, multiple: true, targetEvent: $event, @@ -554,73 +527,7 @@ export class MilestoneService { $event: $event, milestone: milestone, hideStudentWork: hideStudentWork - }, - controller: [ - '$scope', - '$state', - '$mdDialog', - 'milestone', - '$event', - 'TeacherDataService', - function DialogController( - $scope, - $state, - $mdDialog, - milestone, - $event, - TeacherDataService - ) { - $scope.milestone = milestone; - $scope.hideStudentWork = hideStudentWork; - $scope.event = $event; - $scope.close = function () { - $scope.saveMilestoneClosedEvent(); - $mdDialog.hide(); - }; - $scope.edit = function () { - $mdDialog.hide({ - milestone: $scope.milestone, - action: 'edit', - $event: $event - }); - }; - $scope.onShowWorkgroup = function (workgroup: any) { - $scope.saveMilestoneClosedEvent(); - $mdDialog.hide(); - TeacherDataService.setCurrentWorkgroup(workgroup); - $state.go('root.nodeProgress'); - }; - $scope.onVisitNodeGrading = function () { - $mdDialog.hide(); - }; - $scope.saveMilestoneOpenedEvent = function () { - $scope.saveMilestoneEvent('MilestoneOpened'); - }; - $scope.saveMilestoneClosedEvent = function () { - $scope.saveMilestoneEvent('MilestoneClosed'); - }; - $scope.saveMilestoneEvent = function (event: any) { - const context = 'ClassroomMonitor', - nodeId = null, - componentId = null, - componentType = null, - category = 'Navigation', - data = { milestoneId: $scope.milestone.id }, - projectId = null; - TeacherDataService.saveEvent( - context, - nodeId, - componentId, - componentType, - category, - event, - data, - projectId - ); - }; - $scope.saveMilestoneOpenedEvent(); - } - ] + } }); } } diff --git a/src/test/java/org/wise/portal/presentation/web/controllers/user/GoogleUserAPIControllerTest.java b/src/test/java/org/wise/portal/presentation/web/controllers/user/GoogleUserAPIControllerTest.java new file mode 100644 index 0000000000..b66900f8ff --- /dev/null +++ b/src/test/java/org/wise/portal/presentation/web/controllers/user/GoogleUserAPIControllerTest.java @@ -0,0 +1,108 @@ +package org.wise.portal.presentation.web.controllers.user; + +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.HashMap; + +import org.easymock.TestSubject; +import org.junit.Test; +import org.wise.portal.presentation.web.exception.InvalidPasswordExcpetion; + +public class GoogleUserAPIControllerTest extends UserAPIControllerTest { + + @TestSubject + private GoogleUserAPIController controller = new GoogleUserAPIController(); + + @Test + public void isGoogleIdExist_GoogleUserExists_ReturnTrue() { + expect(userService.retrieveUserByGoogleUserId(STUDENT1_GOOGLE_ID)).andReturn(student1); + replay(userService); + assertTrue(controller.isGoogleIdExist(STUDENT1_GOOGLE_ID)); + verify(userService); + } + + @Test + public void isGoogleIdExist_InvalidGoogleUserId_ReturnFalse() { + String invalidGoogleId = "google-id-not-exists-in-db"; + expect(userService.retrieveUserByGoogleUserId(invalidGoogleId)).andReturn(null); + replay(userService); + assertFalse(controller.isGoogleIdExist(invalidGoogleId)); + verify(userService); + } + + @Test + public void isGoogleIdMatches_GoogleUserIdAndUserIdMatch_ReturnTrue() { + expect(userService.retrieveUserByGoogleUserId(STUDENT1_GOOGLE_ID)).andReturn(student1); + replay(userService); + assertTrue(controller.isGoogleIdMatches(STUDENT1_GOOGLE_ID, student1Id.toString())); + verify(userService); + } + + @Test + public void isGoogleIdMatches_InvalidGoogleUserId_ReturnFalse() { + String invalidGoogleId = "google-id-not-exists-in-db"; + expect(userService.retrieveUserByGoogleUserId(invalidGoogleId)).andReturn(null); + replay(userService); + assertFalse(controller.isGoogleIdMatches(invalidGoogleId, student1Id.toString())); + verify(userService); + } + + @Test + public void isGoogleIdMatches_GoogleUserIdAndUserIdDoNotMatch_ReturnFalse() { + expect(userService.retrieveUserByGoogleUserId(STUDENT1_GOOGLE_ID)).andReturn(teacher1); + replay(userService); + assertFalse(controller.isGoogleIdMatches(STUDENT1_GOOGLE_ID, teacher1.toString())); + verify(userService); + } + + @Test + public void getUserByGoogleId_GoogleUserExists_ReturnSuccessResponse() { + expect(userService.retrieveUserByGoogleUserId(STUDENT1_GOOGLE_ID)).andReturn(student1); + replay(userService); + HashMap<String, Object> response = controller.getUserByGoogleId(STUDENT1_GOOGLE_ID); + assertEquals("success", response.get("status")); + assertEquals(student1.getId(), response.get("userId")); + verify(userService); + } + + @Test + public void getUserByGoogleId_InvalidGoogleUserId_ReturnErrorResponse() { + String invalidGoogleId = "google-id-not-exists-in-db"; + expect(userService.retrieveUserByGoogleUserId(invalidGoogleId)).andReturn(null); + replay(userService); + HashMap<String, Object> response = controller.getUserByGoogleId(invalidGoogleId); + assertEquals("error", response.get("status")); + verify(userService); + } + + @Test + public void unlinkGoogleAccount_InvalidNewPassword_ThrowException() { + expect(userService.retrieveUserByUsername(STUDENT_USERNAME)).andReturn(student1); + replay(userService); + String newPass = ""; + try { + controller.unlinkGoogleAccount(studentAuth, newPass); + fail("InvalidPasswordException was expected"); + } catch (Exception e) { + } + } + + @Test + public void unlinkGoogleAccount_ValidNewPassword_ReturnUpdatedUserMap() + throws InvalidPasswordExcpetion { + String newPassword = "my new pass"; + assertTrue(student1.getUserDetails().isGoogleUser()); + expect(userService.retrieveUserByUsername(STUDENT_USERNAME)).andReturn(student1).times(2); + expect(userService.updateUserPassword(student1, newPassword)).andReturn(student1); + expect(appProperties.getProperty("send_email_enabled", "false")).andReturn("false"); + replay(userService, appProperties); + controller.unlinkGoogleAccount(studentAuth, newPassword); + verify(userService, appProperties); + } +} diff --git a/src/test/java/org/wise/portal/presentation/web/controllers/user/UserAPIControllerTest.java b/src/test/java/org/wise/portal/presentation/web/controllers/user/UserAPIControllerTest.java index 251fcb0b47..c5b901ce8f 100644 --- a/src/test/java/org/wise/portal/presentation/web/controllers/user/UserAPIControllerTest.java +++ b/src/test/java/org/wise/portal/presentation/web/controllers/user/UserAPIControllerTest.java @@ -143,68 +143,6 @@ public void getSupportedLanguages_ThreeSupportedLocales_ReturnLanguageArray() { verify(appProperties); } - @Test - public void isGoogleIdExist_GoogleUserExists_ReturnTrue() { - expect(userService.retrieveUserByGoogleUserId(STUDENT1_GOOGLE_ID)).andReturn(student1); - replay(userService); - assertTrue(userAPIController.isGoogleIdExist(STUDENT1_GOOGLE_ID)); - verify(userService); - } - - @Test - public void isGoogleIdExist_InvalidGoogleUserId_ReturnFalse() { - String invalidGoogleId = "google-id-not-exists-in-db"; - expect(userService.retrieveUserByGoogleUserId(invalidGoogleId)).andReturn(null); - replay(userService); - assertFalse(userAPIController.isGoogleIdExist(invalidGoogleId)); - verify(userService); - } - - @Test - public void isGoogleIdMatches_GoogleUserIdAndUserIdMatch_ReturnTrue() { - expect(userService.retrieveUserByGoogleUserId(STUDENT1_GOOGLE_ID)).andReturn(student1); - replay(userService); - assertTrue(userAPIController.isGoogleIdMatches(STUDENT1_GOOGLE_ID, student1Id.toString())); - verify(userService); - } - - @Test - public void isGoogleIdMatches_InvalidGoogleUserId_ReturnFalse() { - String invalidGoogleId = "google-id-not-exists-in-db"; - expect(userService.retrieveUserByGoogleUserId(invalidGoogleId)).andReturn(null); - replay(userService); - assertFalse(userAPIController.isGoogleIdMatches(invalidGoogleId, student1Id.toString())); - verify(userService); - } - - @Test - public void isGoogleIdMatches_GoogleUserIdAndUserIdDoNotMatch_ReturnFalse() { - expect(userService.retrieveUserByGoogleUserId(STUDENT1_GOOGLE_ID)).andReturn(teacher1); - replay(userService); - assertFalse(userAPIController.isGoogleIdMatches(STUDENT1_GOOGLE_ID, teacher1.toString())); - verify(userService); - } - - @Test - public void getUserByGoogleId_GoogleUserExists_ReturnSuccessResponse() { - expect(userService.retrieveUserByGoogleUserId(STUDENT1_GOOGLE_ID)).andReturn(student1); - replay(userService); - HashMap<String, Object> response = userAPIController.getUserByGoogleId(STUDENT1_GOOGLE_ID); - assertEquals("success", response.get("status")); - assertEquals(student1.getId(), response.get("userId")); - verify(userService); - } - - @Test - public void getUserByGoogleId_InvalidGoogleUserId_ReturnErrorResponse() { - String invalidGoogleId = "google-id-not-exists-in-db"; - expect(userService.retrieveUserByGoogleUserId(invalidGoogleId)).andReturn(null); - replay(userService); - HashMap<String, Object> response = userAPIController.getUserByGoogleId(invalidGoogleId); - assertEquals("error", response.get("status")); - verify(userService); - } - @Test public void isNameValid_InvalidName_ReturnFalse() { assertFalse(userAPIController.isNameValid(""));