Skip to content

Commit 4a3cd00

Browse files
authored
Merge pull request #2632 from intersective/2.4.y/CORE-7935/clickable-when-invalid
[CORE-7935] missing object handling
2 parents 8d01b0a + 7f87e37 commit 4a3cd00

12 files changed

Lines changed: 574 additions & 67 deletions

projects/v3/src/app/components/assessment/assessment.component.spec.ts

Lines changed: 374 additions & 1 deletion
Large diffs are not rendered by default.

projects/v3/src/app/components/assessment/assessment.component.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,9 @@ export class AssessmentComponent implements OnInit, OnChanges, OnDestroy {
112112
// if action == 'assessment' and doAssessment is false, it means this user is reading the submission or feedback
113113
doAssessment: boolean;
114114

115+
// tracks whether a submission is in progress to prevent re-enabling the button
116+
private submitting = false;
117+
115118
// if isPendingReview is true, it means this review is WIP, meaning this assessment is pending review
116119
// if action == 'review' and isPendingReview is false, it means the review is done and this student is reading the submission and review
117120
isPendingReview = false;
@@ -409,6 +412,18 @@ Best regards`;
409412
this._initialise();
410413
if (changes.assessment || changes.submission || changes.review) {
411414
this._handleSubmissionData();
415+
416+
// reset submitting flag only when the submission actually changed
417+
// or when the assessment is no longer in an editable state;
418+
// keeps submitting=true during intermediate fetches of the same submission
419+
if (this.submitting) {
420+
const submissionChanged = changes.submission
421+
&& changes.submission.previousValue?.id !== changes.submission.currentValue?.id;
422+
if (submissionChanged || (!this.doAssessment && !this.isPendingReview)) {
423+
this.submitting = false;
424+
}
425+
}
426+
412427
this._populateQuestionsForm();
413428
this._handleReviewData();
414429
this._prefillForm();
@@ -445,7 +460,6 @@ Best regards`;
445460
private _initialise() {
446461
this.doAssessment = false;
447462
this.feedbackReviewed = false;
448-
this.questionsForm = new FormGroup({});
449463
this.isNotInATeam = false;
450464
this.isPendingReview = false;
451465
}
@@ -490,6 +504,10 @@ Best regards`;
490504
// Populate the question form with FormControls.
491505
// The name of form control is like 'q-2' (2 is an example of question id)
492506
private _populateQuestionsForm() {
507+
// build a new form group before assigning to avoid _rawValidators errors
508+
// during template rendering with stale formControlName bindings
509+
const newForm = new FormGroup({});
510+
493511
// question groups
494512
this.assessment.groups.forEach(group => {
495513
// questions in each group
@@ -524,10 +542,13 @@ Best regards`;
524542
}
525543
}
526544

527-
this.questionsForm.addControl('q-' + question.id, new FormControl(quesCtrl, validator));
545+
newForm.addControl('q-' + question.id, new FormControl(quesCtrl, validator));
528546
});
529547
});
530548

549+
// assign fully-built form to trigger a single template update
550+
this.questionsForm = newForm;
551+
531552
// when no questions in the assessment, disable the button
532553
if (this.utils.isEmpty(this.questionsForm.getRawValue())) {
533554
return this.btnDisabled$.next(true);
@@ -666,6 +687,7 @@ Best regards`;
666687
continueToNextTask() {
667688
switch (this._btnAction) {
668689
case 'submit':
690+
this.submitting = true;
669691
this.btnDisabled$.next(true);
670692
return this.submitActions.next({
671693
autoSave: false,
@@ -778,6 +800,7 @@ Best regards`;
778800
const requiredQuestions = this._compulsoryQuestionsAnswered(answers);
779801

780802
if (!autoSave && requiredQuestions.length > 0) {
803+
this.submitting = false;
781804
this.btnDisabled$.next(false);
782805
// display a pop up if required question not answered
783806
return this.notifications.alert({
@@ -805,6 +828,7 @@ Best regards`;
805828
if (this.assessment.isForTeam) {
806829
const teamId = this.storage.getUser().teamId;
807830
if (typeof teamId !== 'number') {
831+
this.submitting = false;
808832
this.btnDisabled$.next(false);
809833
return this.notifications.alert({
810834
message: $localize`Currently you are not in a team, please reach out to your Administrator or Coordinator to proceed with next steps.`,
@@ -818,6 +842,7 @@ Best regards`;
818842
}
819843
}
820844
} catch (error) {
845+
this.submitting = false;
821846
this.btnDisabled$.next(false);
822847
return this.notifications.assessmentSubmittedToast({ isFail: true });
823848
}
@@ -1278,6 +1303,11 @@ Best regards`;
12781303
return;
12791304
}
12801305

1306+
// don't re-enable the button while a submission is in progress
1307+
if (this.submitting) {
1308+
return;
1309+
}
1310+
12811311
const isFormValid = this.questionsForm?.valid ?? false;
12821312
const isCurrentlyDisabled = this.btnDisabled$.getValue();
12831313

projects/v3/src/app/components/file-upload/file-upload.component.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,9 @@ export class FileUploadComponent implements OnInit, OnDestroy {
221221
};
222222
}
223223

224-
this.control.setValue(this.innerValue);
224+
if (this.control) {
225+
this.control.setValue(this.innerValue);
226+
}
225227
this.submitActions$.next(action);
226228
}
227229

@@ -243,8 +245,10 @@ export class FileUploadComponent implements OnInit, OnDestroy {
243245
this.innerValue = this.fileRequestFormat();
244246
}
245247

246-
this.control.setValue(this.innerValue);
247-
this.control.markAsTouched();
248+
if (this.control) {
249+
this.control.setValue(this.innerValue);
250+
this.control.markAsTouched();
251+
}
248252
this.triggerSave();
249253
}
250254

@@ -269,7 +273,7 @@ export class FileUploadComponent implements OnInit, OnDestroy {
269273

270274
// adding save values to from control
271275
private _showSavedAnswers() {
272-
if ((['in progress', 'not start'].includes(this.reviewStatus)) && (this.doReview)) {
276+
if ((['in progress', 'not start'].includes(this.reviewStatus)) && this.doReview && this.review) {
273277
this.innerValue = {
274278
answer: {},
275279
comment: ''
@@ -280,9 +284,11 @@ export class FileUploadComponent implements OnInit, OnDestroy {
280284
this.innerValue.file = this.review.file;
281285
}
282286
if ((this.submissionStatus === 'in progress') && (this.doAssessment)) {
283-
this.innerValue = this.submission.answer;
287+
this.innerValue = this.submission?.answer;
288+
}
289+
if (this.control) {
290+
this.control.setValue(this.innerValue);
284291
}
285-
this.control.setValue(this.innerValue);
286292
}
287293

288294
removeSubmitFile(file?: {

projects/v3/src/app/components/multi-team-member-selector/multi-team-member-selector.component.html

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ <h3 class="for-accessibility" [id]="'multi-team-member-selector-question-' + que
4646
[innerHTML]="teamMember.userName"
4747
[appToggleLabel]="onLabelToggle"
4848
[toggleId]="teamMember.key"
49-
[toggleDisabled]="control.disabled"
49+
[toggleDisabled]="control?.disabled"
5050
role="button"
5151
tabindex="0">
5252
</ion-label>
@@ -58,7 +58,7 @@ <h3 class="for-accessibility" [id]="'multi-team-member-selector-question-' + que
5858
[value]="teamMember.key"
5959
slot="start"
6060
(ionChange)="onChange(teamMember.key)"
61-
[disabled]="control.disabled">
61+
[disabled]="control?.disabled">
6262
</ion-checkbox>
6363
</ion-item>
6464
</ion-list>
@@ -85,12 +85,12 @@ <h3 class="for-accessibility" [id]="'multi-team-member-selector-question-' + que
8585
slot="start"
8686
labelPlacement="end"
8787
(ionChange)="onChange(teamMember.key, 'answer')"
88-
[disabled]="control.disabled">
88+
[disabled]="control?.disabled">
8989
<div class="white-space-normal body-2 black">
9090
<span [innerHTML]="teamMember.userName"
9191
[appToggleLabel]="onLabelToggleReview"
9292
[toggleId]="teamMember.key"
93-
[toggleDisabled]="control.disabled"
93+
[toggleDisabled]="control?.disabled"
9494
role="button"
9595
tabindex="0">
9696
</span>
@@ -104,7 +104,7 @@ <h3 class="for-accessibility" [id]="'multi-team-member-selector-question-' + que
104104
</fieldset>
105105

106106
<ion-list class="ion-no-padding ion-padding-bottom"
107-
*ngIf="question.canComment && submission.answer">
107+
*ngIf="question.canComment && submission?.answer">
108108
<ion-list-header class="ion-no-padding" lines="none">
109109
<ion-label class="subtitle-2 black" i18n>Feedback</ion-label>
110110
</ion-list-header>
@@ -117,7 +117,7 @@ <h3 class="for-accessibility" [id]="'multi-team-member-selector-question-' + que
117117
[(ngModel)]="comment"
118118
(ngModelChange)="onChange(comment, 'comment')"
119119
placeholder="Please put your feedback here"
120-
[disabled]="control.disabled"
120+
[disabled]="control?.disabled"
121121
i18n-placeholder
122122
></ion-textarea>
123123
</ion-list>

projects/v3/src/app/components/multi-team-member-selector/multi-team-member-selector.component.spec.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,4 +181,70 @@ describe('MultiTeamMemberSelectorComponent', () => {
181181
expect(component.audienceContainReviewer()).toBeFalsy();
182182
});
183183
});
184+
185+
describe('isSelectedInSubmission()', () => {
186+
const teamMember = { key: JSON.stringify({ name: 'User1', recipientId: 1, recipientEmail: 'u1@test.com', userId: 10 }), userName: 'User1' };
187+
188+
it('should return false when submission is null', () => {
189+
component.submission = null;
190+
expect(component.isSelectedInSubmission(teamMember)).toBeFalse();
191+
});
192+
193+
it('should return false when submission is undefined', () => {
194+
component.submission = undefined;
195+
expect(component.isSelectedInSubmission(teamMember)).toBeFalse();
196+
});
197+
198+
it('should return false when submission.answer is null', () => {
199+
component.submission = { answer: null };
200+
expect(component.isSelectedInSubmission(teamMember)).toBeFalse();
201+
});
202+
203+
it('should return true when team member is selected in submission answer', () => {
204+
component.submission = {
205+
answer: [JSON.stringify({ name: 'User1', recipientId: 1, recipientEmail: 'u1@test.com', userId: 10 })],
206+
};
207+
expect(component.isSelectedInSubmission(teamMember)).toBeTrue();
208+
});
209+
210+
it('should return false when team member is not selected in submission answer', () => {
211+
component.submission = {
212+
answer: [JSON.stringify({ name: 'Other', recipientId: 2, recipientEmail: 'o@test.com', userId: 99 })],
213+
};
214+
expect(component.isSelectedInSubmission(teamMember)).toBeFalse();
215+
});
216+
});
217+
218+
describe('isSelectedInReview()', () => {
219+
const teamMember = { key: JSON.stringify({ name: 'User1', recipientId: 1, recipientEmail: 'u1@test.com', userId: 10 }), userName: 'User1' };
220+
221+
it('should return false when review is null', () => {
222+
component.review = null;
223+
expect(component.isSelectedInReview(teamMember)).toBeFalse();
224+
});
225+
226+
it('should return false when review is undefined', () => {
227+
component.review = undefined;
228+
expect(component.isSelectedInReview(teamMember)).toBeFalse();
229+
});
230+
231+
it('should return false when review.answer is null', () => {
232+
component.review = { answer: null };
233+
expect(component.isSelectedInReview(teamMember)).toBeFalse();
234+
});
235+
236+
it('should return true when team member is selected in review answer', () => {
237+
component.review = {
238+
answer: [JSON.stringify({ name: 'User1', recipientId: 1, recipientEmail: 'u1@test.com', userId: 10 })],
239+
};
240+
expect(component.isSelectedInReview(teamMember)).toBeTrue();
241+
});
242+
243+
it('should return false when team member is not selected in review answer', () => {
244+
component.review = {
245+
answer: [JSON.stringify({ name: 'Other', recipientId: 2, recipientEmail: 'o@test.com', userId: 99 })],
246+
};
247+
expect(component.isSelectedInReview(teamMember)).toBeFalse();
248+
});
249+
});
184250
});

projects/v3/src/app/components/multi-team-member-selector/multi-team-member-selector.component.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -116,11 +116,13 @@ export class MultiTeamMemberSelectorComponent implements ControlValueAccessor, O
116116
// reset errors
117117
this.errors = [];
118118
// setting, resetting error messages into an array (to loop) and adding the validation messages to show below the answer area
119-
for (const key in this.control.errors) {
120-
if (key === 'required') {
121-
this.errors.push('This question is required');
122-
} else {
123-
this.errors.push(this.control.errors[key]);
119+
if (this.control?.errors) {
120+
for (const key in this.control.errors) {
121+
if (key === 'required') {
122+
this.errors.push('This question is required');
123+
} else {
124+
this.errors.push(this.control.errors[key]);
125+
}
124126
}
125127
}
126128

@@ -146,7 +148,7 @@ export class MultiTeamMemberSelectorComponent implements ControlValueAccessor, O
146148

147149
// adding save values to from control
148150
private _showSavedAnswers() {
149-
if ((['in progress', 'not start'].includes(this.reviewStatus)) && this.doReview) {
151+
if ((['in progress', 'not start'].includes(this.reviewStatus)) && this.doReview && this.review) {
150152
this.innerValue = {
151153
answer: this.review.answer || [],
152154
comment: this.review.comment
@@ -155,10 +157,12 @@ export class MultiTeamMemberSelectorComponent implements ControlValueAccessor, O
155157
} else if ((this.submissionStatus === 'in progress') && this.doAssessment) {
156158
if (!this.innerValue) {
157159
this.innerValue = {
158-
answer: this.submission.answer || [],
160+
answer: this.submission?.answer || [],
159161
};
160162
}
161-
this.innerValue.answer = this.control.pristine ? this.submission.answer : this.control.value;
163+
if (this.control) {
164+
this.innerValue.answer = this.control.pristine ? this.submission?.answer : this.control.value;
165+
}
162166
}
163167

164168
this.propagateChange(this.innerValue);

projects/v3/src/app/components/multiple/multiple.component.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -148,11 +148,13 @@ export class MultipleComponent implements AfterViewInit, ControlValueAccessor, O
148148
// reset errors
149149
this.errors = [];
150150
// setting, resetting error messages into an array (to loop) and adding the validation messages to show below the answer area
151-
for (const key in this.control.errors) {
152-
if (key === 'required') {
153-
this.errors.push('This question is required');
154-
} else {
155-
this.errors.push(this.control.errors[key]);
151+
if (this.control?.errors) {
152+
for (const key in this.control.errors) {
153+
if (key === 'required') {
154+
this.errors.push('This question is required');
155+
} else {
156+
this.errors.push(this.control.errors[key]);
157+
}
156158
}
157159
}
158160

@@ -174,15 +176,19 @@ export class MultipleComponent implements AfterViewInit, ControlValueAccessor, O
174176
}
175177
// adding save values to from control
176178
private _showSavedAnswers() {
177-
if ((['in progress', 'not start'].includes(this.reviewStatus)) && this.doReview) {
179+
if ((['in progress', 'not start'].includes(this.reviewStatus)) && this.doReview && this.review) {
178180
this.innerValue = {
179181
answer: this.review.answer,
180182
comment: this.review.comment
181183
};
182184
this.comment = this.review.comment;
183185
}
184186
if ((this.submissionStatus === 'in progress') && this.doAssessment) {
185-
this.innerValue = this.control.pristine ? this.submission.answer : this.control.value;
187+
if (this.control) {
188+
this.innerValue = this.control.pristine ? this.submission?.answer : this.control.value;
189+
} else {
190+
this.innerValue = this.submission?.answer;
191+
}
186192
}
187193
this.propagateChange(this.innerValue);
188194
}

0 commit comments

Comments
 (0)