Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#11878] Upgrade instructor request form UI #12929

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
9ad6ffa
Add confirmation prompt
xenosf Mar 19, 2024
7be6abe
Remove old form iframe
xenosf Mar 19, 2024
f5fc131
Improve declaration view spacing
xenosf Mar 19, 2024
7f8fce6
Edit page heading phrasing for clarity
xenosf Mar 20, 2024
97bc62a
Create request form
xenosf Mar 20, 2024
3b957d9
Add validation messages
xenosf Mar 20, 2024
fc2adad
Fix form validation
xenosf Mar 20, 2024
f3845cc
Set up form submission confirmation
xenosf Mar 23, 2024
293275f
Create submission acknowledgement view
xenosf Mar 23, 2024
ad7eaf4
Fix URL checking regex
xenosf Mar 23, 2024
79bd9ce
Fix initial state
xenosf Mar 23, 2024
12a7551
Display placeholder when optional field is empty
xenosf Mar 23, 2024
760a825
Fix code style
xenosf Mar 23, 2024
9f62318
Edit comment for clarity
xenosf Mar 23, 2024
1e4549a
Fix institution and country combination
xenosf Mar 26, 2024
e5080fb
Fix naming
xenosf Mar 26, 2024
b213f4d
Remove hard line break
xenosf Mar 26, 2024
2107d3c
Add explanatory comment for regex
xenosf Mar 26, 2024
2b155a0
Remove newline
xenosf Mar 26, 2024
3a11abf
Add newlines at end of file
xenosf Mar 26, 2024
0493ddd
Clear styles file
xenosf Mar 26, 2024
8733e12
Re-add styles file
xenosf Mar 26, 2024
e5908b7
Include test
xenosf Mar 26, 2024
d043af2
Add test cases for requestSubmissionEvent
xenosf Mar 26, 2024
c1226e9
Improve test case readability
xenosf Mar 26, 2024
5369ad3
Edit test case name for clarity
xenosf Mar 26, 2024
ec7395d
Add snapshot tests
xenosf Mar 26, 2024
9744ee5
Revert "Add snapshot tests"
xenosf Mar 26, 2024
51b0281
Fix lint errors
xenosf Mar 26, 2024
3192c87
Merge branch 'account-request-form' into arf-instructor-frontend
xenosf Mar 26, 2024
13e9a9e
Rename methods to be clearer
xenosf Mar 26, 2024
a08d997
Disable submit button when not ready to submit
xenosf Mar 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export type InstructorRequestFormModel = {
name: string,
institution: string,
country: string,
email: string,
homePage: string,
comments: string,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<p aria-hidden="true">
<!-- aria-hidden as screen readers use inputs' required attribute instead (and cannot see the asterisks) -->
Questions marked with an asterisk <span class="red-font">*</span> are required.
</p>
<form (ngSubmit)="onSubmit()" [formGroup]="arf">
<div class="form-group {{checkIsFieldRequired(name) ? 'required' : ''}}">
<label for="name" id="name-label" class="qn">
Full Name
</label>
<p class="help-block">
This is the name that will be shown to your students. You may include salutation (Dr. Prof. etc.)
</p>
<input class="form-control {{getFieldValidationClasses(name)}}" type="text" id="name" autocomplete="name"
[formControl]="name" [required]="checkIsFieldRequired(name)" [attr.aria-invalid]="checkIsFieldInvalid(name)">
<div *ngIf="checkIsFieldInvalid(name)" role="alert" aria-describedby="name-label" tabindex="0" class="invalid-feedback">
Please enter your name.
</div>
</div>
<br>
<div class="form-group {{checkIsFieldRequired(institution) ? 'required' : ''}}">
<label for="institution" id="institution-label" class="qn">
University/school/institution
</label>
<p class="help-block">
Please give full name of the university/institution.
</p>
<input class="form-control {{getFieldValidationClasses(institution)}}" type="text" id="institution"
autocomplete="organization" [formControl]="institution" [required]="checkIsFieldRequired(institution)"
[attr.aria-invalid]="checkIsFieldInvalid(institution)">
<div *ngIf="checkIsFieldInvalid(institution)" role="alert" aria-describedby="institution-label" tabindex="0"
class="invalid-feedback">
Please enter your institution.
</div>
</div>
<br>
<div class="form-group {{checkIsFieldRequired(country) ? 'required' : ''}}">
<label for="country" id="country-label" class="qn">
Country
</label>
<p class="help-block">
Which country is your university/institution based in?
</p>
<input class="form-control {{getFieldValidationClasses(country)}}" type="text" id="country"
autocomplete="country-name" [formControl]="country" [required]="checkIsFieldRequired(country)"
[attr.aria-invalid]="checkIsFieldInvalid(country)">
<div *ngIf="checkIsFieldInvalid(country)" role="alert" aria-describedby="country-label" tabindex="0"
class="invalid-feedback">
Please enter your institution's country.
</div>
</div>
<br>
<div class="form-group {{checkIsFieldRequired(email) ? 'required' : ''}}">
<label for="email" id="email-label" class="qn">
Official email address
</label>
<p class="help-block">
Please use the email address <b>given to you by your school/university</b>
(not your personal Gmail/Hotmail address).
Note that this email address will be visible to the students you enroll in TEAMMATES.
</p>
<input class="form-control {{getFieldValidationClasses(email)}}" type="email" id="email" autocomplete="email"
[formControl]="email" [required]="checkIsFieldRequired(email)" [attr.aria-invalid]="checkIsFieldInvalid(email)">
<div *ngIf="checkIsFieldInvalid(email)" role="alert" aria-describedby="email-label" tabindex="0"
class="invalid-feedback">
Please enter a valid email address.
</div>
</div>
<br>
<div class="form-group {{checkIsFieldRequired(homePage) ? 'required' : ''}}">
<label for="homePage" id="homePage-label" class="qn">
URL of your home page (if any)
</label>
<input class="form-control {{getFieldValidationClasses(homePage)}}" type="url" id="homePage" autocomplete="url"
[formControl]="homePage" [required]="checkIsFieldRequired(homePage)" [attr.aria-invalid]="checkIsFieldInvalid(homePage)">
<div *ngIf="checkIsFieldInvalid(homePage)" role="alert" aria-describedby="homePage-label" tabindex="0"
class="invalid-feedback">
Please enter a valid URL.
</div>
</div>
<br>
<div class="form-group {{checkIsFieldRequired(comments) ? 'required' : ''}}">
<label for="comments" id="comments-label" class="qn">
Any other comments/queries
</label>
<textarea class="form-control {{getFieldValidationClasses(comments)}}" [formControl]="comments"
[attr.aria-invalid]="checkIsFieldInvalid(comments)"></textarea>
</div>
<br>
<button type="submit" class="btn btn-primary" id="submit-button" [disabled]="!checkCanSubmit()">
Submit
</button>
</form>
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
label.qn {
font-weight: bold;
font-size: 1rem;
margin-bottom: 0.3rem;
}

.form-group {
margin-bottom: 0.5rem;
}

.form-group.required > label::after {
content:"*";
color: red;
}

.help-block {
margin-bottom: 0.8rem;
}

.red-font {
color: red;
}
xenosf marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { first } from 'rxjs';
import { InstructorRequestFormModel } from './instructor-request-form-model';
import { InstructorRequestFormComponent } from './instructor-request-form.component';

describe('InstructorRequestFormComponent', () => {
let component: InstructorRequestFormComponent;
let fixture: ComponentFixture<InstructorRequestFormComponent>;
const typicalModel: InstructorRequestFormModel = {
name: 'John Doe',
institution: 'Example Institution',
country: 'Example Country',
email: 'jd@example.edu',
homePage: 'xyz.example.edu/john',
comments: '',
};

/**
* Fills in form fields with the given data.
*
* @param data Data to fill form with.
*/
function fillFormWith(data: InstructorRequestFormModel): void {
component.name.setValue(data.name);
component.institution.setValue(data.institution);
component.country.setValue(data.country);
component.email.setValue(data.email);
component.homePage.setValue(data.homePage);
component.comments.setValue(data.comments);
}

beforeEach(() => {
TestBed.configureTestingModule({
declarations: [InstructorRequestFormComponent],
imports: [ReactiveFormsModule],
});
fixture = TestBed.createComponent(InstructorRequestFormComponent);
component = fixture.componentInstance;

fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});

it('should emit requestSubmissionEvent once when submit button is clicked', () => {
jest.spyOn(component.requestSubmissionEvent, 'emit');

fillFormWith(typicalModel);
const submitButton = fixture.debugElement.query(By.css('#submit-button'));
submitButton.nativeElement.click();

expect(component.requestSubmissionEvent.emit).toHaveBeenCalledTimes(1);
});

it('should emit requestSubmissionEvent with the correct data when form is submitted', () => {
// Listen for emitted value
let actualModel: InstructorRequestFormModel | null = null;
component.requestSubmissionEvent.pipe(first())
.subscribe((data: InstructorRequestFormModel) => { actualModel = data; });

fillFormWith(typicalModel);
component.onSubmit();

expect(actualModel).toBeTruthy();
expect(actualModel!.name).toBe(typicalModel.name);
expect(actualModel!.institution).toBe(typicalModel.institution);
expect(actualModel!.country).toBe(typicalModel.country);
expect(actualModel!.email).toBe(typicalModel.email);
expect(actualModel!.homePage).toBe(typicalModel.homePage);
expect(actualModel!.comments).toBe(typicalModel.comments);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { Component, EventEmitter, Output } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { InstructorRequestFormModel } from './instructor-request-form-model';

// Use regex to validate URL field as Angular does not have a built-in URL validator
// eslint-disable-next-line
const URL_REGEX = /(https?:\/\/)?(www\.)[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)|(https?:\/\/)?(www\.)?(?!ww)[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/;
xenosf marked this conversation as resolved.
Show resolved Hide resolved

@Component({
selector: 'tm-instructor-request-form',
templateUrl: './instructor-request-form.component.html',
styleUrls: ['./instructor-request-form.component.scss'],
})
export class InstructorRequestFormComponent {

arf = new FormGroup({
name: new FormControl('', [Validators.required]),
institution: new FormControl('', [Validators.required]),
country: new FormControl('', [Validators.required]),
email: new FormControl('', [Validators.required, Validators.email]),
homePage: new FormControl('', [Validators.pattern(URL_REGEX)]),
comments: new FormControl(''),
}, { updateOn: 'submit' });

// Create members for easier access of arf controls
name = this.arf.controls.name;
institution = this.arf.controls.institution;
country = this.arf.controls.country;
email = this.arf.controls.email;
homePage = this.arf.controls.homePage;
comments = this.arf.controls.comments;

hasSubmitAttempt = false;

@Output() requestSubmissionEvent = new EventEmitter<InstructorRequestFormModel>();

checkIsFieldRequired(field: FormControl): boolean {
return field.hasValidator(Validators.required);
}

checkIsFieldInvalid(field: FormControl): boolean {
return field.invalid;
}

checkCanSubmit(): boolean {
return true; // TODO: API integration
}

getFieldValidationClasses(field: FormControl): string {
let str = '';
if (this.hasSubmitAttempt) {
if (field.invalid) {
str = 'is-invalid';
} else if (field.value !== '') {
str = 'is-valid';
}
}
return str;
}

onSubmit(): void {
this.hasSubmitAttempt = true;

if (this.arf.invalid) {
// Do not submit form
return;
}

const name = this.name.value!.trim();
const email = this.email.value!.trim();
const country = this.country.value!.trim();
const institution = this.institution.value!.trim();
const combinedInstitution = `${institution}, ${country}`;
const homePage = this.homePage.value!;
const comments = this.comments.value!.trim();

const submittedData = {
name,
email,
institution: combinedInstitution,
homePage,
comments,
};
// TODO: connect to API
// eslint-disable-next-line
submittedData; // PLACEHOLDER

// Pass form input to parent to display confirmation
this.requestSubmissionEvent.emit({
name,
institution,
country,
email,
homePage,
comments,
});
}
}
74 changes: 63 additions & 11 deletions src/web/app/pages-static/request-page/request-page.component.html
Original file line number Diff line number Diff line change
@@ -1,14 +1,66 @@
<h1 class="color-orange">
Request for an Account
Request for an Instructor Account
</h1>
<div *ngIf="accountRequestFormUrl">
<p>
Cannot see the request form below? <a [href]="accountRequestFormUrl" target="_blank" rel="noopener noreferrer">Click here.</a>
</p>
<iframe [src]="accountRequestFormUrl" width="760px" height="880px" frameborder="0" marginheight="0" marginwidth="0">
Loading...
</iframe>
</div>
<div *ngIf="!accountRequestFormUrl">
The URL for the account request form is not set.
<div class="col-xs-12 col-md-10 col-lg-8 col-xl-7 col-xxl-6">
<div *ngIf="!submittedFormData">
<p>
Request for an instructor account using this form if you are an instructor and want to use TEAMMATES to manage peer evaluations and/or other feedback paths of your students.
</p>
<hr>
<div *ngIf="!isDeclarationDone">
<p>
Note: <b>Students should not use this form to request for TEAMMATES accounts</b>, as students do not need accounts to use TEAMMATES. Instead, TEAMMATES will email students (who have been added to TEAMMATES by a course instructor) an access link when there is a TEAMMATES session available for them to access.
</p>
<a type="button" class="btn btn-secondary" tmRouterLink="/web/front/home">Back to home page</a>
<button type="button" class="btn btn-primary ms-3" (click)="onDeclarationButtonClicked()">I am an instructor</button>
</div>
<div *ngIf="isDeclarationDone">
<tm-instructor-request-form *ngIf="!submittedFormData" (requestSubmissionEvent)="onRequestSubmitted($event)"></tm-instructor-request-form>
</div>
<hr>
</div>
<div *ngIf="submittedFormData">
<p>
Your request has been submitted successfully:
</p>
<table class="table table-bordered my-3">
<tbody>
<tr>
<th scope="row" class="col-3">Full Name</th>
<td>{{submittedFormData.name}}</td>
</tr>
<tr>
<th scope="row">Institution</th>
<td>{{submittedFormData.institution}}</td>
</tr>
<tr>
<th scope="row">Country</th>
<td>{{submittedFormData.country}}</td>
</tr>
<tr>
<th scope="row">Email</th>
<td>{{submittedFormData.email}}</td>
</tr>
<tr>
<th scope="row">Home Page URL</th>
<td>
{{submittedFormData.homePage}}
<span class="empty-field-placeholder" *ngIf="!submittedFormData.homePage"></span>
</td>
</tr>
<tr>
<th scope="row">Comments</th>
<td>
{{submittedFormData.comments}}
<span class="empty-field-placeholder" *ngIf="!submittedFormData.comments"></span>
</td>
</tr>
</tbody>
</table>
<p>
We have sent an acknowledgement email to your email address <b>{{submittedFormData.email}}</b>.
Please check your email inbox or spam folder.
If you do not receive the acknowledgement email within 1 hour, please <a tmRouterLink="/web/front/contact">contact</a> us.
</p>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.empty-field-placeholder::after {
content: "(empty)";
opacity: 0.5;
font-style: italic;
}
Loading
Loading