Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,21 @@ <h3 [attr.aria-describedby]="randomCode(group.name)">{{ group.name }}</h3>
</ion-item>

<ng-container *ngIf="question" [ngSwitch]="question.type">
<div class="tick-container">
<ion-spinner name="dots" class="tick-icon"
style="max-width: 16px; max-height: 16px;"
*ngIf="autosaving[question.id]"
[@tickAnimation]="autosaving[question.id] ? 'visible' : 'hidden'"
(@tickAnimation.done)="onAnimationEnd($event, question.id)"
></ion-spinner>

<ion-icon name="checkmark-circle-outline"
class="tick-icon"
*ngIf="saved[question.id]"
[@tickAnimation]="saved[question.id] ? 'visible' : 'hidden'"
></ion-icon>
</div>

<ion-item *ngIf="!doAssessment && (
(!question.reviewerOnly && utils.isEmpty(submission?.answers[question.id]?.answer)) ||
(question.reviewerOnly && !review?.answers[question.id] && !isPendingReview)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,10 @@ ion-footer {
margin-left: auto;
margin-right: auto;
}

.tick-container {
position: absolute;
bottom: 1px;
right: 1px;
z-index: 1;
}
36 changes: 34 additions & 2 deletions projects/v3/src/app/components/assessment/assessment.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,23 @@ import { NotificationsService } from '@v3/services/notifications.service';
import { FormGroup, FormControl, Validators } from '@angular/forms';
import { BrowserStorageService } from '@v3/services/storage.service';
import { SharedService } from '@v3/services/shared.service';
import { BehaviorSubject, Observable, of, Subject, Subscription, throwError } from 'rxjs';
import { concatMap, delay, filter, takeUntil, tap } from 'rxjs/operators';
import { BehaviorSubject, Observable, of, Subject, Subscription, timer } from 'rxjs';
import { concatMap, take, delay, filter, takeUntil, tap } from 'rxjs/operators';
import { trigger, state, style, animate, transition } from '@angular/animations';

// const SAVE_PROGRESS_TIMEOUT = 10000; - AV2-1326
@Component({
selector: 'app-assessment',
templateUrl: './assessment.component.html',
styleUrls: ['./assessment.component.scss'],
animations: [
trigger('tickAnimation', [
state('visible', style({ transform: 'scale(1)', opacity: 1 })),
state('hidden', style({ transform: 'scale(0)', opacity: 0 })),
transition('hidden => visible', animate('200ms ease-out')),
transition('visible => hidden', animate('100ms ease-in')),
]),
],
})
export class AssessmentComponent implements OnInit, OnChanges, OnDestroy {
/**
Expand Down Expand Up @@ -46,6 +55,22 @@ export class AssessmentComponent implements OnInit, OnChanges, OnDestroy {
// continue to the next task
@Output() continue = new EventEmitter();

autosaving: {
[key: number]: boolean
} = {};
saved: {
[key:number]: boolean
} = {};

onAnimationEnd(event, questionId: number) {
if (event.toState === 'visible') {
// Animation has ended with the tick being visible, now toggle the saved flag after a short delay
timer(1000).pipe(take(1)).subscribe(() => {
this.autosaving[questionId] = false;
});
}
}

// used to resubscribe to the assessment service
resubscribe$ = new Subject();
// used to save the assessment/review answers
Expand Down Expand Up @@ -115,9 +140,12 @@ export class AssessmentComponent implements OnInit, OnChanges, OnDestroy {
filter(() => !this._preventSubmission()), // skip when false
concatMap(request => {
if (request?.reviewSave) {
// this.saved[request.reviewSave.questionId] = true;
return this.saveReviewAnswer(request.reviewSave);
}
if (request?.questionSave) {
this.autosaving[request.questionSave.questionId] = true;
this.saved[request.questionSave.questionId] = false;
return this.saveQuestionAnswer(request.questionSave);
}
return of(request);
Expand Down Expand Up @@ -192,6 +220,10 @@ export class AssessmentComponent implements OnInit, OnChanges, OnDestroy {
questionInput.questionId,
answer,
).pipe(
tap((_res) => {
this.autosaving[questionInput.questionId] = false;
this.saved[questionInput.questionId] = true;
}),
delay(800)
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<img [src]="storage.getConfig().logo || './assets/logo.svg'" width="100%" [attr.alt]="(!storage.getConfig().logo) ? 'Practera logo' : (name || 'branding logo')" i18n-alt>
<img [src]="logo || './assets/logo.svg'" width="100%" [attr.alt]="!logo ? 'Practera logo' : (name || 'branding logo')" i18n-alt>
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,7 @@ export class BrandingLogoComponent {
@Input() logo: string;
@Input() name?: string;

constructor(public storage: BrowserStorageService) {}
constructor(public storage: BrowserStorageService) {
this.logo = this.logo || this.storage.getConfig().logo;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ <h3 class="for-accessibility" [id]="'multiple-choice-question-' + question.id">{
></ion-checkbox>
</ion-item>
</ion-list>

<ion-textarea
[attr.aria-label]="'expert\'s review feedback'"
*ngIf="question.canComment && submission.answer"
Expand Down
84 changes: 50 additions & 34 deletions projects/v3/src/app/components/multiple/multiple.component.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Component, Input, Output, EventEmitter, forwardRef, ViewChild, ElementRef, OnInit } from '@angular/core';
import { Component, Input, Output, EventEmitter, forwardRef, ViewChild, ElementRef, OnInit, QueryList, OnDestroy, ViewChildren } from '@angular/core';
import { NG_VALUE_ACCESSOR, ControlValueAccessor, AbstractControl } from '@angular/forms';
import { IonCheckbox } from '@ionic/angular';
import { UtilsService } from '@v3/app/services/utils.service';
import { Subject } from 'rxjs';
import { from, fromEvent, merge, Subject, Subscription } from 'rxjs';
import { debounceTime, map, switchMap } from 'rxjs/operators';

@Component({
selector: 'app-multiple',
Expand All @@ -15,7 +17,7 @@ import { Subject } from 'rxjs';
}
]
})
export class MultipleComponent implements ControlValueAccessor, OnInit {
export class MultipleComponent implements ControlValueAccessor, OnInit, OnDestroy {
@Input() submitActions$: Subject<any>;

@Input() question;
Expand All @@ -33,17 +35,19 @@ export class MultipleComponent implements ControlValueAccessor, OnInit {
@Input() doReview: Boolean;
// FormControl that is passed in from parent component
@Input() control: AbstractControl;
// answer field for submitter & reviewer
@ViewChild('answer') answerRef: ElementRef;
// comment field for reviewer
@ViewChild('commentEle') commentRef: ElementRef;

autosave$ = new Subject<any>();

// the value of answer
innerValue: any;
comment: string;
// validation errors array
errors: Array<any> = [];

subscriptions: Subscription[] = [];

constructor(
private utils: UtilsService,
) {}
Expand All @@ -52,6 +56,46 @@ export class MultipleComponent implements ControlValueAccessor, OnInit {
this._showSavedAnswers();
}

ngAfterViewInit() {
this.autosave$.pipe(
debounceTime(800),
).subscribe(() => {
const action: {
autoSave?: boolean;
goBack?: boolean;
questionSave?: {};
reviewSave?: {};
} = {
autoSave: true,
goBack: false,
};

if (this.doReview === true) {
action.reviewSave = {
reviewId: this.reviewId,
submissionId: this.submissionId,
questionId: this.question.id,
answer: this.innerValue.answer,
comment: this.innerValue.comment,
};
}

if (this.doAssessment === true) {
action.questionSave = {
submissionId: this.submissionId,
questionId: this.question.id,
answer: this.innerValue,
};
}

this.submitActions$.next(action);
});
}

ngOnDestroy() {
this.subscriptions.forEach(sub => sub.unsubscribe());
}

// propagate changes into the form control
propagateChange = (_: any) => {};

Expand Down Expand Up @@ -106,35 +150,7 @@ export class MultipleComponent implements ControlValueAccessor, OnInit {
}
}

const action: {
autoSave?: boolean;
goBack?: boolean;
questionSave?: {};
reviewSave?: {};
} = {
autoSave: true,
goBack: false,
};

if (this.doReview === true) {
action.reviewSave = {
reviewId: this.reviewId,
submissionId: this.submissionId,
questionId: this.question.id,
answer: this.innerValue.answer,
comment: this.innerValue.comment,
};
}

if (this.doAssessment === true) {
action.questionSave = {
submissionId: this.submissionId,
questionId: this.question.id,
answer: this.innerValue,
};
}

this.submitActions$.next(action);
this.autosave$.next();
}

// From ControlValueAccessor interface
Expand Down
73 changes: 42 additions & 31 deletions projects/v3/src/app/components/oneof/oneof.component.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Component, Input, Output, EventEmitter, forwardRef, ViewChild, ElementRef, OnInit } from '@angular/core';
import { NG_VALUE_ACCESSOR, ControlValueAccessor, FormControl, AbstractControl } from '@angular/forms';
import { Component, Input, forwardRef, ViewChild, ElementRef, OnInit } from '@angular/core';
import { NG_VALUE_ACCESSOR, ControlValueAccessor, AbstractControl } from '@angular/forms';
import { Subject } from 'rxjs';
import { debounceTime } from 'rxjs/operators';

@Component({
selector: 'app-oneof',
Expand Down Expand Up @@ -43,12 +44,50 @@ export class OneofComponent implements ControlValueAccessor, OnInit {
// validation errors array
errors: Array<any> = [];

autosave$ = new Subject<any>();

constructor() {}

ngOnInit() {
this._showSavedAnswers();
}

ngAfterViewInit() {
this.autosave$.pipe(
debounceTime(800),
).subscribe(() => {
const action: {
autoSave?: boolean;
goBack?: boolean;
questionSave?: {};
reviewSave?: {};
} = {
autoSave: true,
goBack: false,
};

if (this.doReview === true) {
action.reviewSave = {
reviewId: this.reviewId,
submissionId: this.submissionId,
questionId: this.question.id,
answer: this.innerValue.answer,
comment: this.innerValue.comment,
};
}

if (this.doAssessment === true) {
action.questionSave = {
submissionId: this.submissionId,
questionId: this.question.id,
answer: this.innerValue,
};
}

this.submitActions$.next(action);
});
}

// propagate changes into the form control
propagateChange = (_: any) => {};

Expand Down Expand Up @@ -83,35 +122,7 @@ export class OneofComponent implements ControlValueAccessor, OnInit {
}
}

const action: {
autoSave?: boolean;
goBack?: boolean;
questionSave?: {};
reviewSave?: {};
} = {
autoSave: true,
goBack: false,
};

if (this.doReview === true) {
action.reviewSave = {
reviewId: this.reviewId,
submissionId: this.submissionId,
questionId: this.question.id,
answer: this.innerValue.answer,
comment: this.innerValue.comment,
};
}

if (this.doAssessment === true) {
action.questionSave = {
submissionId: this.submissionId,
questionId: this.question.id,
answer: this.innerValue,
};
}

this.submitActions$.next(action);
this.autosave$.next();
}

// From ControlValueAccessor interface
Expand Down
10 changes: 4 additions & 6 deletions projects/v3/src/app/components/text/text.component.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Component, Input, forwardRef, ViewChild, ElementRef, OnInit, AfterViewInit, OnDestroy } from '@angular/core';
import { NG_VALUE_ACCESSOR, ControlValueAccessor, FormControl, AbstractControl } from '@angular/forms';
import { IonTextarea } from '@ionic/angular';
import { AssessmentService, Question } from '@v3/services/assessment.service';
import { Question } from '@v3/services/assessment.service';
import { Subject, Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged, filter, map } from 'rxjs/operators';

Expand Down Expand Up @@ -48,9 +48,7 @@ export class TextComponent implements ControlValueAccessor, OnInit, AfterViewIni
// validation errors array
errors: Array<any> = [];

constructor(
private assessmentService: AssessmentService,
) {}
constructor() {}

ngOnInit() {
this._showSavedAnswers();
Expand All @@ -60,8 +58,8 @@ export class TextComponent implements ControlValueAccessor, OnInit, AfterViewIni
if (this.answerRef?.ionInput) {
this.subcriptions.push(this.answerRef.ionInput.pipe(
map(e => (e.target as HTMLInputElement).value),
filter(text => text.length > 0),
debounceTime(1250),
filter(text => text.length >= 0),
debounceTime(800),
distinctUntilChanged(),
).subscribe(_data => {
const action: {
Expand Down
Loading