Skip to content

[CORE-5930] Task/save indicator - golive/3.2.4 #1936

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

Open
wants to merge 11 commits into
base: golive/3.2.4
Choose a base branch
from
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