diff --git a/src/web/app/components/question-submission-form/question-submission-form.component.html b/src/web/app/components/question-submission-form/question-submission-form.component.html
index 7c07540e8ee..e2d079b9840 100644
--- a/src/web/app/components/question-submission-form/question-submission-form.component.html
+++ b/src/web/app/components/question-submission-form/question-submission-form.component.html
@@ -241,6 +241,8 @@
Question {{ model.questionNumber }}: {{ mode
{{ isQuestionCountOne ? "Submit Response" : "Submit Response for Question " + model.questionNumber }}
+
diff --git a/src/web/app/components/question-submission-form/question-submission-form.component.ts b/src/web/app/components/question-submission-form/question-submission-form.component.ts
index 21ff6f00d99..59d83d25227 100644
--- a/src/web/app/components/question-submission-form/question-submission-form.component.ts
+++ b/src/web/app/components/question-submission-form/question-submission-form.component.ts
@@ -93,6 +93,7 @@ export class QuestionSubmissionFormComponent implements DoCheck {
this.model.isTabExpandedForRecipients.set(recipient.recipientIdentifier, true);
});
+ this.hasResponseChanged = Array.from(this.model.hasResponseChangedForRecipients.values()).some((value) => value);
}
@Input()
@@ -118,6 +119,12 @@ export class QuestionSubmissionFormComponent implements DoCheck {
@Output()
responsesSave: EventEmitter = new EventEmitter();
+ @Output()
+ autoSave: EventEmitter<{ id: string, model: QuestionSubmissionFormModel }> = new EventEmitter();
+
+ @Output()
+ resetFeedback: EventEmitter = new EventEmitter();
+
@ViewChild(ContributionQuestionConstraintComponent)
private contributionQuestionConstraint!: ContributionQuestionConstraintComponent;
@@ -168,6 +175,8 @@ export class QuestionSubmissionFormComponent implements DoCheck {
visibilityStateMachine: VisibilityStateMachine;
isEveryRecipientSorted: boolean = false;
+ autosaveTimeout: any;
+
constructor(private feedbackQuestionsService: FeedbackQuestionsService,
private feedbackResponseService: FeedbackResponsesService) {
this.visibilityStateMachine =
@@ -221,6 +230,13 @@ export class QuestionSubmissionFormComponent implements DoCheck {
});
}
+ resetForm(): void {
+ this.resetFeedback.emit(this.model);
+ this.isSaved = true;
+ this.hasResponseChanged = false;
+ clearTimeout(this.autosaveTimeout);
+ }
+
toggleQuestionTab(): void {
if (this.currentSelectedSessionView === this.allSessionViews.DEFAULT) {
this.model.isTabExpanded = !this.model.isTabExpanded;
@@ -350,7 +366,6 @@ export class QuestionSubmissionFormComponent implements DoCheck {
*/
triggerRecipientSubmissionFormChange(index: number, field: string, data: any): void {
if (!this.isFormsDisabled) {
- this.hasResponseChanged = true;
this.isSubmitAllClickedChange.emit(false);
this.model.hasResponseChangedForRecipients.set(this.model.recipientList[index].recipientIdentifier, true);
@@ -362,6 +377,12 @@ export class QuestionSubmissionFormComponent implements DoCheck {
this.updateIsValidByQuestionConstraint();
this.formModelChange.emit(this.model);
+
+ this.autoSave.emit({ id: this.model.feedbackQuestionId, model: this.model });
+ clearTimeout(this.autosaveTimeout);
+ this.autosaveTimeout = setTimeout(() => {
+ this.hasResponseChanged = true;
+ }, 100); // 0.1 second to prevent people from trying to immediately reset before autosave kicks in
}
}
@@ -451,6 +472,7 @@ export class QuestionSubmissionFormComponent implements DoCheck {
* Triggers saving of responses for the specific question.
*/
saveFeedbackResponses(): void {
+ clearTimeout(this.autosaveTimeout);
this.isSaved = true;
this.hasResponseChanged = false;
this.model.hasResponseChangedForRecipients.forEach(
diff --git a/src/web/app/pages-session/session-submission-page/__snapshots__/session-submission-page.component.spec.ts.snap b/src/web/app/pages-session/session-submission-page/__snapshots__/session-submission-page.component.spec.ts.snap
index 5110ed14b92..d0f974f68b8 100644
--- a/src/web/app/pages-session/session-submission-page/__snapshots__/session-submission-page.component.spec.ts.snap
+++ b/src/web/app/pages-session/session-submission-page/__snapshots__/session-submission-page.component.spec.ts.snap
@@ -2,11 +2,13 @@
exports[`SessionSubmissionPageComponent should snap when feedback session questions have failed to load 1`] = `
+
@@ -1547,6 +1571,14 @@ exports[`SessionSubmissionPageComponent should snap with feedback session questi
Submit Response for Question 3
+
@@ -1980,6 +2012,14 @@ exports[`SessionSubmissionPageComponent should snap with feedback session questi
Submit Response for Question 4
+
@@ -2195,6 +2235,14 @@ exports[`SessionSubmissionPageComponent should snap with feedback session questi
Submit Response for Question 5
+
@@ -2424,6 +2472,14 @@ exports[`SessionSubmissionPageComponent should snap with feedback session questi
Submit Response for Question 6
+
@@ -2883,6 +2939,14 @@ exports[`SessionSubmissionPageComponent should snap with feedback session questi
Submit Response for Question 7
+
@@ -3227,6 +3291,14 @@ exports[`SessionSubmissionPageComponent should snap with feedback session questi
Submit Response for Question 8
+
@@ -3509,6 +3581,14 @@ exports[`SessionSubmissionPageComponent should snap with feedback session questi
Submit Response for Question 9
+
@@ -3741,6 +3821,14 @@ exports[`SessionSubmissionPageComponent should snap with feedback session questi
Submit Response for Question 10
+
@@ -3770,11 +3858,13 @@ exports[`SessionSubmissionPageComponent should snap with feedback session questi
exports[`SessionSubmissionPageComponent should snap with feedback session question submission forms when disabled 1`] = `
+
@@ -4563,6 +4662,13 @@ exports[`SessionSubmissionPageComponent should snap with feedback session questi
Submit Response for Question 3
+
@@ -5002,6 +5108,14 @@ exports[`SessionSubmissionPageComponent should snap with feedback session questi
Submit Response for Question 4
+
@@ -5219,6 +5333,14 @@ exports[`SessionSubmissionPageComponent should snap with feedback session questi
Submit Response for Question 5
+
@@ -5450,6 +5572,14 @@ exports[`SessionSubmissionPageComponent should snap with feedback session questi
Submit Response for Question 6
+
@@ -5911,6 +6041,14 @@ exports[`SessionSubmissionPageComponent should snap with feedback session questi
Submit Response for Question 7
+
@@ -6265,6 +6403,14 @@ exports[`SessionSubmissionPageComponent should snap with feedback session questi
Submit Response for Question 8
+
@@ -6549,6 +6695,14 @@ exports[`SessionSubmissionPageComponent should snap with feedback session questi
Submit Response for Question 9
+
@@ -6782,6 +6936,14 @@ exports[`SessionSubmissionPageComponent should snap with feedback session questi
Submit Response for Question 10
+
@@ -6812,11 +6974,13 @@ exports[`SessionSubmissionPageComponent should snap with feedback session questi
exports[`SessionSubmissionPageComponent should snap with user that is logged in and using session link 1`] = `
{{ getRecipientName(entry.key) }}
[(isSubmitAllClicked)]="isSubmitAllClicked"
[currentSelectedSessionView]="currentSelectedSessionView"
[recipientId]="entry.key"
+ (resetFeedback)="resetFeedbackResponses([$event], entry.key)"
+ (autoSave)="handleAutoSave($event)"
>
@@ -133,6 +135,9 @@ {{ getRecipientName(entry.key) }}
(click)="saveResponsesForSelectedRecipientQuestions(entry.key, questionSubmissionForms)"
[disabled]="isSavingResponses || isSubmissionFormsDisabled">Submit Responses for {{ getRecipientName(entry.key) }}
+
@@ -151,6 +156,8 @@ There are no ungroupable questions
(deleteCommentEvent)="deleteParticipantComment(i, $event)"
[isQuestionCountOne]="isQuestionCountOne"
[(isSubmitAllClicked)]="isSubmitAllClicked"
+ (resetFeedback)="resetFeedbackResponses([$event], null)"
+ (autoSave)="handleAutoSave($event)"
>
@@ -166,6 +173,8 @@ There are no ungroupable questions
[isQuestionCountOne]="isQuestionCountOne"
[(isSubmitAllClicked)]="isSubmitAllClicked"
[currentSelectedSessionView]="currentSelectedSessionView"
+ (resetFeedback)="resetFeedbackResponses([$event], null)"
+ (autoSave)="handleAutoSave($event)"
>
diff --git a/src/web/app/pages-session/session-submission-page/session-submission-page.component.spec.ts b/src/web/app/pages-session/session-submission-page/session-submission-page.component.spec.ts
index e83ce3462dd..389870d2cb5 100644
--- a/src/web/app/pages-session/session-submission-page/session-submission-page.component.spec.ts
+++ b/src/web/app/pages-session/session-submission-page/session-submission-page.component.spec.ts
@@ -1281,4 +1281,38 @@ describe('SessionSubmissionPageComponent', () => {
expect(commentSpy).toHaveBeenLastCalledWith(expectedId, Intent.STUDENT_SUBMISSION,
{ key: testQueryParams.key, moderatedperson: '' });
});
+
+ it('should autosave data to localStorage', () => {
+ const questionId = 'feedback-question-id-mcq';
+ const model: QuestionSubmissionFormModel = deepCopy(testMcqQuestionSubmissionForm);
+ model.hasResponseChangedForRecipients = new Map().set('r1', true);
+ model.isTabExpandedForRecipients = new Map().set('r1', true);
+ const event = { id: questionId, model };
+ const setItemSpy = jest.spyOn(Storage.prototype, 'setItem');
+
+ jest.useFakeTimers();
+ component.handleAutoSave(event);
+ jest.advanceTimersByTime(component.autoSaveDelay);
+
+ expect(setItemSpy).toHaveBeenCalled();
+ jest.useRealTimers();
+ });
+
+ it('should load autosaved data from localStorage', () => {
+ const questionId = 'feedback-question-id-mcq';
+ const savedModel: any = deepCopy(testMcqQuestionSubmissionForm);
+ savedModel.hasResponseChangedForRecipients = Array.from(new Map().set('r1', true).entries());
+ savedModel.isTabExpandedForRecipients = Array.from(new Map().set('r1', true).entries());
+
+ const getItemSpy = jest.spyOn(Storage.prototype, 'getItem')
+ .mockReturnValue(JSON.stringify({ [questionId]: savedModel }));
+
+ component.questionSubmissionForms = [deepCopy(testMcqQuestionSubmissionForm)];
+
+ component.loadAutoSavedData(questionId);
+
+ expect(component.questionSubmissionForms[0].hasResponseChangedForRecipients.get('r1')).toBe(true);
+ expect(component.questionSubmissionForms[0].isTabExpandedForRecipients.get('r1')).toBe(true);
+ expect(getItemSpy).toHaveBeenCalledWith('autosave');
+ });
});
diff --git a/src/web/app/pages-session/session-submission-page/session-submission-page.component.ts b/src/web/app/pages-session/session-submission-page/session-submission-page.component.ts
index 3761289f162..0703cd4cf40 100644
--- a/src/web/app/pages-session/session-submission-page/session-submission-page.component.ts
+++ b/src/web/app/pages-session/session-submission-page/session-submission-page.component.ts
@@ -103,6 +103,7 @@ export class SessionSubmissionPageComponent implements OnInit, AfterViewInit {
intent: Intent = Intent.STUDENT_SUBMISSION;
questionSubmissionForms: QuestionSubmissionFormModel[] = [];
+ originalQuestionSubmissionForms: QuestionSubmissionFormModel[] = [];
isSavingResponses: boolean = false;
isSubmissionFormsDisabled: boolean = false;
@@ -130,8 +131,13 @@ export class SessionSubmissionPageComponent implements OnInit, AfterViewInit {
feedbackSessionId: string | undefined = '';
studentId: string | undefined = '';
+ autoSaveTimeout: any;
+ autoSaveDelay = 100; // 0.1 second delay
+
private backendUrl: string = environment.backendUrl;
+ private readonly AUTOSAVE_KEY = 'autosave';
+
constructor(private route: ActivatedRoute,
private statusMessageService: StatusMessageService,
private timezoneService: TimezoneService,
@@ -152,6 +158,44 @@ export class SessionSubmissionPageComponent implements OnInit, AfterViewInit {
this.timezoneService.getTzVersion(); // import timezone service to load timezone data
}
+ handleAutoSave(event: { id: string, model: QuestionSubmissionFormModel }): void {
+ // Disable autosave in preview mode
+ if (this.previewAsPerson) {
+ return;
+ }
+
+ clearTimeout(this.autoSaveTimeout);
+ this.autoSaveTimeout = setTimeout(() => {
+ const savedData = this.getLocalStorageItem(this.AUTOSAVE_KEY);
+ const clonedModel = {
+ ...event.model,
+ hasResponseChangedForRecipients: Array.from(event.model.hasResponseChangedForRecipients.entries()),
+ isTabExpandedForRecipients: Array.from(event.model.isTabExpandedForRecipients.entries()),
+ };
+ savedData[event.id] = clonedModel;
+ this.setLocalStorageItem(this.AUTOSAVE_KEY, savedData);
+ }, this.autoSaveDelay);
+ }
+
+ loadAutoSavedData(questionId: string): void {
+ // Disable loading autosaved data in preview mode
+ if (this.previewAsPerson) {
+ return;
+ }
+
+ const savedData = this.getLocalStorageItem(this.AUTOSAVE_KEY);
+ const savedModel = savedData[questionId];
+
+ if (savedModel) {
+ const index = this.questionSubmissionForms.findIndex((q) => q.feedbackQuestionId === questionId);
+ if (index !== -1) {
+ savedModel.hasResponseChangedForRecipients = new Map(savedModel.hasResponseChangedForRecipients);
+ savedModel.isTabExpandedForRecipients = new Map(savedModel.isTabExpandedForRecipients);
+ this.questionSubmissionForms[index] = savedModel;
+ }
+ }
+ }
+
ngOnInit(): void {
this.route.data.pipe(
tap((data: any) => {
@@ -641,6 +685,20 @@ export class SessionSubmissionPageComponent implements OnInit, AfterViewInit {
}).pipe(finalize(() => {
model.isLoading = false;
model.isLoaded = true;
+
+ this.originalQuestionSubmissionForms.push({
+ ...model,
+ hasResponseChangedForRecipients: new Map(model.hasResponseChangedForRecipients),
+ isTabExpandedForRecipients: new Map(model.isTabExpandedForRecipients),
+ recipientList: model.recipientList.map((recipient) => ({ ...recipient })),
+ recipientSubmissionForms: model.recipientSubmissionForms.map((form) => ({
+ ...form,
+ responseDetails: { ...form.responseDetails },
+ commentByGiver: form.commentByGiver ? { ...form.commentByGiver } : undefined,
+ })),
+ questionDetails: { ...model.questionDetails },
+ });
+
}))
.subscribe({
next: (existingResponses: FeedbackResponsesResponse) => {
@@ -819,6 +877,30 @@ export class SessionSubmissionPageComponent implements OnInit, AfterViewInit {
recipientSubmissionFormModel.commentByGiver = undefined;
}
});
+
+ const savedData = this.getLocalStorageItem(this.AUTOSAVE_KEY);
+ delete savedData[questionSubmissionFormModel.feedbackQuestionId];
+ this.setLocalStorageItem(this.AUTOSAVE_KEY, savedData);
+
+ this.originalQuestionSubmissionForms.forEach((originalModel: QuestionSubmissionFormModel) => {
+ if (originalModel.feedbackQuestionId === questionSubmissionFormModel.feedbackQuestionId) {
+ originalModel.recipientSubmissionForms.forEach((originalRecipientSubmissionFormModel:
+ FeedbackResponseRecipientSubmissionFormModel) => {
+ if (responsesMap[originalRecipientSubmissionFormModel.recipientIdentifier]) {
+ const correspondingResp: FeedbackResponse =
+ responsesMap[originalRecipientSubmissionFormModel.recipientIdentifier];
+ originalRecipientSubmissionFormModel.responseId = correspondingResp.feedbackResponseId;
+ originalRecipientSubmissionFormModel.responseDetails = correspondingResp.responseDetails;
+ originalRecipientSubmissionFormModel.recipientIdentifier =
+ correspondingResp.recipientIdentifier;
+ } else {
+ originalRecipientSubmissionFormModel.responseId = '';
+ originalRecipientSubmissionFormModel.commentByGiver = undefined;
+ }
+ });
+ }
+
+ });
}),
switchMap(() =>
forkJoin(questionSubmissionFormModel.recipientSubmissionForms
@@ -992,6 +1074,7 @@ export class SessionSubmissionPageComponent implements OnInit, AfterViewInit {
if (event && event.visible && !questionSubmissionForm.isLoaded && !questionSubmissionForm.isLoading) {
questionSubmissionForm.isLoading = true;
this.loadFeedbackQuestionRecipientsForQuestion(questionSubmissionForm);
+ this.loadAutoSavedData(questionSubmissionForm.feedbackQuestionId);
}
}
@@ -1028,6 +1111,101 @@ export class SessionSubmissionPageComponent implements OnInit, AfterViewInit {
this.saveFeedbackResponses(recipientQSForms, false, recipientId);
}
+ resetResponsesForSelectedRecipientQuestions(recipientId: string,
+ questionSubmissionForms: QuestionSubmissionFormModel[]): void {
+
+ const questionsToRecipient: Set | undefined = this.recipientQuestionMap.get(recipientId);
+ if (!questionsToRecipient) {
+ this.statusMessageService.showErrorToast('Failed to reset response for this recipient. '
+ + 'Please switch back to "Group by Question" view to reset responses.');
+ }
+ const recipientQSForms = questionSubmissionForms
+ .filter((questionSubmissionFormModel: QuestionSubmissionFormModel) =>
+ questionsToRecipient!.has(questionSubmissionFormModel.questionNumber));
+ this.resetFeedbackResponses(recipientQSForms, recipientId);
+ }
+
+ resetFeedbackResponses(questionSubmissionForms: QuestionSubmissionFormModel[], recipientId: string | null): void {
+ const savedData = this.getLocalStorageItem(this.AUTOSAVE_KEY);
+
+ questionSubmissionForms.forEach((questionSubmissionFormModel: QuestionSubmissionFormModel) => {
+ const originalSubmissionForm = this.originalQuestionSubmissionForms.find(
+ (originalModel: QuestionSubmissionFormModel) =>
+ originalModel.feedbackQuestionId === questionSubmissionFormModel.feedbackQuestionId,
+ );
+
+ if (originalSubmissionForm) {
+ if (recipientId) {
+ questionSubmissionFormModel.recipientSubmissionForms.forEach((form, index) => {
+ if (form.recipientIdentifier === recipientId) {
+ const originalForm = originalSubmissionForm.recipientSubmissionForms.find(
+ (originalRecipientForm) => originalRecipientForm.recipientIdentifier === form.recipientIdentifier,
+ );
+
+ if (originalForm) {
+ questionSubmissionFormModel.recipientSubmissionForms[index] = {
+ ...originalForm,
+ responseDetails: { ...originalForm.responseDetails },
+ commentByGiver: originalForm.commentByGiver ? { ...originalForm.commentByGiver } : undefined,
+ };
+ }
+ }
+ });
+
+ questionSubmissionFormModel.hasResponseChangedForRecipients.set(
+ recipientId, originalSubmissionForm.hasResponseChangedForRecipients.get(recipientId) ?? false,
+ );
+ questionSubmissionFormModel.isTabExpandedForRecipients.set(
+ recipientId, originalSubmissionForm.isTabExpandedForRecipients.get(recipientId) ?? true,
+ );
+
+ if (savedData[questionSubmissionFormModel.feedbackQuestionId]) {
+ const recipientIndex = savedData[questionSubmissionFormModel.feedbackQuestionId].recipientSubmissionForms
+ .findIndex((form: FeedbackResponseRecipientSubmissionFormModel) =>
+ form.recipientIdentifier === recipientId);
+
+ if (recipientIndex !== -1) {
+ savedData[questionSubmissionFormModel.feedbackQuestionId]
+ .recipientSubmissionForms.splice(recipientIndex, 1);
+ }
+
+ if (savedData[questionSubmissionFormModel.feedbackQuestionId].recipientSubmissionForms.length === 0) {
+ delete savedData[questionSubmissionFormModel.feedbackQuestionId];
+ }
+ }
+ } else {
+ Object.assign(questionSubmissionFormModel, {
+ ...originalSubmissionForm,
+ recipientSubmissionForms: originalSubmissionForm.recipientSubmissionForms
+ .map((form: FeedbackResponseRecipientSubmissionFormModel) => ({
+ ...form,
+ responseDetails: { ...form.responseDetails },
+ commentByGiver: form.commentByGiver ? { ...form.commentByGiver } : undefined,
+ })),
+ hasResponseChangedForRecipients: new Map(originalSubmissionForm.hasResponseChangedForRecipients),
+ isTabExpandedForRecipients: new Map(originalSubmissionForm.isTabExpandedForRecipients),
+ questionDetails: { ...originalSubmissionForm.questionDetails },
+ });
+
+ delete savedData[questionSubmissionFormModel.feedbackQuestionId];
+ }
+ }
+ });
+
+ this.setLocalStorageItem(this.AUTOSAVE_KEY, savedData);
+ }
+
+ hasResponseChangedForRecipient(recipientId: string,
+ questionSubmissionForms: QuestionSubmissionFormModel[]): boolean {
+ const questionsToRecipient: Set | undefined = this.recipientQuestionMap.get(recipientId);
+ if (!questionsToRecipient) {
+ return false;
+ }
+ return questionSubmissionForms.some((questionSubmissionFormModel: QuestionSubmissionFormModel) =>
+ questionsToRecipient.has(questionSubmissionFormModel.questionNumber)
+ && questionSubmissionFormModel.hasResponseChangedForRecipients.get(recipientId));
+ }
+
private addQuestionForRecipient(recipientId: string, questionId: any): void {
if (this.recipientQuestionMap.has(recipientId)) {
this.recipientQuestionMap.get(recipientId)!.add(questionId);
@@ -1153,4 +1331,18 @@ export class SessionSubmissionPageComponent implements OnInit, AfterViewInit {
studentId: this.studentId,
}).subscribe();
}
+
+ /**
+ * Utility method to get item from local storage.
+ */
+ private getLocalStorageItem(key: string): any {
+ return JSON.parse(localStorage.getItem(key) || '{}');
+ }
+
+ /**
+ * Utility method to set item in local storage.
+ */
+ private setLocalStorageItem(key: string, data: any): void {
+ localStorage.setItem(key, JSON.stringify(data));
+ }
}