Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1ddf936
[CORE-7940] pulsecheck race-condition
trtshen Sep 10, 2025
72f0da5
Merge pull request #2538 from intersective/2.4.y/CORE-7940/double-pul…
shawnm0705 Sep 10, 2025
2223cdb
Merge pull request #2544 from intersective/2.4.y/CORE-8012/slider-bro…
trtshen Sep 23, 2025
9f91f27
[CORE-8026] add setTimer to delay validation check before form is pop…
trtshen Sep 26, 2025
1685b3d
[CORE-7944] added exception for mobile review
trtshen Sep 25, 2025
d5c7057
[CORE-8027] reduce prev & next gap
trtshen Sep 29, 2025
3284f10
[CORE-8028] increased pagination indicator size
trtshen Sep 30, 2025
11072c7
[CORE-8029] don't flick
trtshen Sep 30, 2025
61440dc
[CORE-8028] removed question quanitiy indicator
trtshen Oct 1, 2025
4d4313c
[CORE-8031] readonly avoid required indicator
trtshen Oct 3, 2025
7d5a322
Merge pull request #2561 from intersective/2.4.7/CORE-8032/mobile-vie…
trtshen Oct 15, 2025
11f89f7
Merge pull request #2548 from intersective/2.4.y/CORE-8011/flexible-r…
trtshen Oct 15, 2025
d8bff26
Merge pull request #2556 from intersective/2.4.y/CORE-8029/theme-colo…
trtshen Oct 15, 2025
c1f2c3f
Merge remote-tracking branch 'origin/prerelease' into golive/2.4.y
trtshen Nov 20, 2025
79fdaf7
hide fab button temporarily in activity-desktop page
trtshen Feb 26, 2026
35590c3
Merge pull request #2622 from intersective/hide-fab-button
trtshen Feb 26, 2026
536b60c
Merge pull request #2623 from intersective/2.4.y.z/CORE-8002/pulseche…
shawnm0705 Feb 27, 2026
cb9981e
[CORE-7940] fix: release fastFeedbackOpening lock on dismiss and destroy
trtshen Mar 3, 2026
e0dd31d
Merge pull request #2628 from intersective/2.4.y.z/CORE-7940/persiste…
trtshen Mar 3, 2026
d7c4f15
Merge pull request #2627 from intersective/prerelease
shawnm0705 Mar 3, 2026
ee71703
Merge remote-tracking branch 'origin/golive/2.4.7' into 2.4.y.z/missi…
trtshen Mar 3, 2026
35dc8ea
Merge pull request #2629 from intersective/2.4.y.z/missing-merges
trtshen Mar 6, 2026
a71605c
Merge remote-tracking branch 'origin/release/live' into golive/2.4.y
trtshen Mar 9, 2026
0f0dbf1
Merge remote-tracking branch 'origin/prerelease' into golive/2.4.y
trtshen Mar 9, 2026
15c1324
Merge remote-tracking branch 'origin/golive/2.4.y' into 2.4.y/CORE-74…
trtshen Mar 9, 2026
ae7c57d
Merge remote-tracking branch 'origin/2.4.y/CORE-7496/assessment-pagin…
trtshen Mar 9, 2026
34bda99
Merge remote-tracking branch 'origin/trunk' into 2.4.y/CORE-7496/asse…
trtshen Mar 9, 2026
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ projects/v3/src/environments/environment.prod.ts

.cursor/rules/nx-rules.mdc
.github/instructions/nx.instructions.md
output/

logs_49068925926

Expand Down
155 changes: 109 additions & 46 deletions projects/v3/src/app/components/assessment/assessment.component.html
Original file line number Diff line number Diff line change
@@ -1,26 +1,46 @@
<div *ngIf="!assessment" class="ion-padding-horizontal" role="status" aria-label="Loading assessment" i18n-aria-label aria-live="polite">
<ion-skeleton-text animated style="width: 50%;"></ion-skeleton-text>
<div *ngIf="!assessment"
class="ion-padding-horizontal"
role="status"
aria-label="Loading assessment"
i18n-aria-label
aria-live="polite">
<ion-skeleton-text animated
style="width: 50%;"></ion-skeleton-text>
<ion-skeleton-text animated></ion-skeleton-text>
<ion-skeleton-text animated></ion-skeleton-text>
</div>

<!-- Manual Save -->
<div class="save-info" *ngIf="assessment && (doAssessment || isPendingReview)" role="status" aria-live="polite" aria-atomic="true">
<div class="save-info"
*ngIf="assessment && (doAssessment || isPendingReview)"
role="status"
aria-live="polite"
aria-atomic="true">
<p class="body-2 grey-75">{{ savingMessage$ | async }}</p>
</div>

<ng-container *ngIf="action==='assessment' && submission?.status === 'feedback available'">
<ion-list class="ion-no-margin ion-no-padding ion-padding-horizontal" *ngIf="submission.reviewerName" role="region" aria-label="Reviewer information" i18n-aria-label>
<ion-list class="ion-no-margin ion-no-padding ion-padding-horizontal"
*ngIf="submission.reviewerName"
role="region"
aria-label="Reviewer information"
i18n-aria-label>
<ion-item-group>
<ion-item-divider>
<ion-label color="primary"
i18n>Reviewer Details</ion-label>
</ion-item-divider>
<ion-item class="ion-no-padding">
<ion-grid class="ion-padding-horizontal">
<ion-row size="12" class="ion-justify-content-center ion-align-items-center">
<ion-col size="6" class="ion-align-self-center" i18n>Expert</ion-col>
<ion-col size="6" class="ion-align-self-center" [innerHTML]="submission.reviewerName" aria-label="Expert name"></ion-col>
<ion-row size="12"
class="ion-justify-content-center ion-align-items-center">
<ion-col size="6"
class="ion-align-self-center"
i18n>Expert</ion-col>
<ion-col size="6"
class="ion-align-self-center"
[innerHTML]="submission.reviewerName"
aria-label="Expert name"></ion-col>
</ion-row>
</ion-grid>
</ion-item>
Expand All @@ -30,29 +50,42 @@

<ng-container *ngIf="action === 'review'">
<ion-list *ngIf="submission"
class="ion-no-margin ion-padding ion-padding-horizontal main-content" role="region" aria-label="Submission information" i18n-aria-label>
class="ion-no-margin ion-padding ion-padding-horizontal main-content"
role="region"
aria-label="Submission information"
i18n-aria-label>
<ion-item-group>
<ion-item-divider>
<ion-label color="primary"
i18n>Submission Details</ion-label>
</ion-item-divider>
<ion-item class="ion-no-padding">
<ion-grid class="ion-padding-horizontal">
<ion-row size="12" class="ion-justify-content-center ion-align-items-center">
<ion-col size="6" class="ion-align-self-center" i18n>Learner</ion-col>
<ion-col size="6" class="ion-align-self-center"
[innerHTML]="submission.submitterName" aria-label="Learner name"></ion-col>
<ion-row size="12"
class="ion-justify-content-center ion-align-items-center">
<ion-col size="6"
class="ion-align-self-center"
i18n>Learner</ion-col>
<ion-col size="6"
class="ion-align-self-center"
[innerHTML]="submission.submitterName"
aria-label="Learner name"></ion-col>
</ion-row>
</ion-grid>
</ion-item>

<ion-item class="ion-no-padding"
*ngIf="review?.teamName">
<ion-grid class="ion-padding-horizontal">
<ion-row size="12" class="ion-justify-content-center ion-align-items-center">
<ion-col size="6" class="ion-align-self-center" i18n>Team</ion-col>
<ion-col size="6" class="ion-align-self-center"
[innerHTML]="review.teamName" aria-label="Team name"></ion-col>
<ion-row size="12"
class="ion-justify-content-center ion-align-items-center">
<ion-col size="6"
class="ion-align-self-center"
i18n>Team</ion-col>
<ion-col size="6"
class="ion-align-self-center"
[innerHTML]="review.teamName"
aria-label="Team name"></ion-col>
</ion-row>
</ion-grid>
</ion-item>
Expand Down Expand Up @@ -90,11 +123,17 @@
aria-level="2"
[innerHTML]="assessment.name"></div>

<ion-chip [class]="'label body-3 '+ labelColor" *ngIf="label" role="status">
<ion-label [innerHTML]="label" aria-label="Assessment status"></ion-label>
<ion-chip [class]="'label body-3 '+ labelColor"
*ngIf="label"
role="status">
<ion-label [innerHTML]="label"
aria-label="Assessment status"></ion-label>
</ion-chip>

<p class="assessment subtitle-1 due-date" *ngIf="assessment.dueDate" style="margin: 10px 0;" role="note">
<p class="assessment subtitle-1 due-date"
*ngIf="assessment.dueDate"
style="margin: 10px 0;"
role="note">
<strong i18n>Due Date</strong>: <time [attr.datetime]="assessment.dueDate">{{ utils.utcToLocal(assessment.dueDate, 'timeZone') }}</time>
</p>

Expand All @@ -107,7 +146,11 @@
[attr.aria-describedby]="randomCode(assessment.name)"></app-description>
</ion-text>

<ion-list *ngIf="submission?.isLocked" class="member-detail-container no-bg" lines="none" role="alert" aria-live="assertive">
<ion-list *ngIf="submission?.isLocked"
class="member-detail-container no-bg"
lines="none"
role="alert"
aria-live="assertive">
<ion-item lines="none">
<ion-avatar slot="start">
<img [src]="submission.submitterImage ? submission.submitterImage : 'https://my.practera.com/img/user-512.png'"
Expand All @@ -124,13 +167,22 @@
</ion-list>
</div>

<form [formGroup]="questionsForm" id="formgroup" #form aria-label="Assessment form" i18n-aria-label novalidate>
<form [formGroup]="questionsForm"
id="formgroup"
#form
aria-label="Assessment form"
i18n-aria-label
novalidate>
<ng-container *ngFor="let group of pagedGroups; let groupIndex = index">
<div [ngClass]="{'ion-padding': isSinglePage, 'ion-padding-top': !isSinglePage}" role="group" [attr.aria-labelledby]="'group-heading-' + groupIndex">
<h3 [id]="'group-heading-' + groupIndex" [attr.aria-describedby]="randomCode(group.name)" [innerHTML]="group.name"></h3>
<ion-text color="dark" class="ion-text-left">
<div
*ngIf="group.description"
<div [ngClass]="{'ion-padding': isSinglePage, 'ion-padding-top': !isSinglePage}"
role="group"
[attr.aria-labelledby]="'group-heading-' + groupIndex">
<h3 [id]="'group-heading-' + groupIndex"
[attr.aria-describedby]="randomCode(group.name)"
[innerHTML]="group.name"></h3>
<ion-text color="dark"
class="ion-text-left">
<div *ngIf="group.description"
class="g-descriptiond body-2"
[id]="randomCode(group.name)"
[innerHtml]="group.description">
Expand All @@ -152,8 +204,8 @@ <h3 [id]="'group-heading-' + groupIndex" [attr.aria-describedby]="randomCode(gro
<span class="required-mark"
[ngClass]="{'contrast': isRedColor}"
*ngIf="shouldShowRequiredIndicator(question)"
aria-label="required" i18n-aria-label
>&nbsp;*</span>
aria-label="required"
i18n-aria-label>&nbsp;*</span>
</ion-label>

<ion-button mode="ios"
Expand All @@ -163,9 +215,11 @@ <h3 [id]="'group-heading-' + groupIndex" [attr.aria-describedby]="randomCode(gro
(keydown.enter)="showQuestionInfo(question.info, $event)"
(keydown.space)="showQuestionInfo(question.info, $event); $event.preventDefault()"
class="btn-info ion-no-margin focusable"
[attr.aria-label]="'Show question information'" i18n-aria-label
[attr.aria-label]="'Show question information'"
i18n-aria-label
tabindex="0">
<ion-icon name="information-circle-outline" aria-hidden="true"></ion-icon>
<ion-icon name="information-circle-outline"
aria-hidden="true"></ion-icon>
</ion-button>
</ion-list-header>

Expand All @@ -179,24 +233,29 @@ <h3 [id]="'group-heading-' + groupIndex" [attr.aria-describedby]="randomCode(gro
</div>
</ion-item>

<ng-container *ngIf="question" [ngSwitch]="question.type">
<div class="tick-container" role="status" aria-live="polite" [attr.aria-label]="autosaving()[question.id] ? 'Saving' : (saved()[question.id] ? 'Saved successfully' : (failed()[question.id] ? 'Save failed' : ''))">
<ion-spinner name="dots" class="tick-icon"
<ng-container *ngIf="question"
[ngSwitch]="question.type">
<div class="tick-container"
role="status"
aria-live="polite"
[attr.aria-label]="autosaving()[question.id] ? 'Saving' : (saved()[question.id] ? 'Saved successfully' : (failed()[question.id] ? 'Save failed' : ''))">
<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)"
aria-label="Saving answer" i18n-aria-label
role="img"
></ion-spinner>
aria-label="Saving answer"
i18n-aria-label
role="img"></ion-spinner>

<ion-icon name="checkmark-circle-outline"
class="tick-icon"
*ngIf="saved()[question.id]"
[@tickAnimation]="saved()[question.id] ? 'visible' : 'hidden'"
aria-label="Answer saved successfully" i18n-aria-label
role="img"
></ion-icon>
aria-label="Answer saved successfully"
i18n-aria-label
role="img"></ion-icon>

<ion-icon name="reload-circle-outline"
class="tick-icon"
Expand All @@ -207,8 +266,8 @@ <h3 [id]="'group-heading-' + groupIndex" [attr.aria-describedby]="randomCode(gro
[@tickAnimation]="failed()[question.id] ? 'visible' : 'hidden'"
tabindex="0"
role="button"
[attr.aria-label]="'Retry save'" i18n-aria-label
></ion-icon>
[attr.aria-label]="'Retry save'"
i18n-aria-label></ion-icon>
</div>

<ng-container *ngIf="question.type !== 'slider'">
Expand All @@ -223,8 +282,7 @@ <h3 [id]="'group-heading-' + groupIndex" [attr.aria-describedby]="randomCode(gro
color="orange"
name="information-circle-outline"
class="ion-margin-end"
aria-hidden="true"
></ion-icon>
aria-hidden="true"></ion-icon>
<ion-label size="small"
i18n>No answer for this question.</ion-label>
</ion-item>
Expand Down Expand Up @@ -351,8 +409,11 @@ <h3 [id]="'group-heading-' + groupIndex" [attr.aria-describedby]="randomCode(gro
[control]="questionsForm?.controls['q-' + question.id]"
[submitActions$]="submitActions"></app-multi-team-member-selector>

<div *ngSwitchDefault role="alert">
<p class="ion-padding-horizontal" style="color:red;" i18n>Unsupported question type: {{ question.type }}</p>
<div *ngSwitchDefault
role="alert">
<p class="ion-padding-horizontal"
style="color:red;"
i18n>Unsupported question type: {{ question.type }}</p>
</div>
</ng-container>
</ion-list>
Expand All @@ -363,7 +424,9 @@ <h3 [id]="'group-heading-' + groupIndex" [attr.aria-describedby]="randomCode(gro
</div>

<div class="not-team ion-padding ion-text-center"
*ngIf="isNotInATeam" role="alert" aria-live="polite">
*ngIf="isNotInATeam"
role="alert"
aria-live="polite">
<p class="black subtitle-1"
i18n>Currently you are not in a team, please reach out to your Administrator or Coordinator to
proceed with next steps.</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,17 @@ describe('FastFeedbackComponent', () => {
expect(Object.keys(component.fastFeedbackForm.controls).length).toBe(5);
});

it('when testing dismiss(), it should dismiss', () => {
it('when testing dismiss(), it should dismiss and release lock', () => {
const storageSpy = TestBed.inject(BrowserStorageService) as jasmine.SpyObj<BrowserStorageService>;
component.dismiss({});
expect(modalSpy.dismiss.calls.count()).toBe(1);
expect(storageSpy.set).toHaveBeenCalledWith('fastFeedbackOpening', false);
});

it('ngOnDestroy() should release fastFeedbackOpening lock', () => {
const storageSpy = TestBed.inject(BrowserStorageService) as jasmine.SpyObj<BrowserStorageService>;
component.ngOnDestroy();
expect(storageSpy.set).toHaveBeenCalledWith('fastFeedbackOpening', false);
});

describe('when testing submit()', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,10 @@ export class FastFeedbackComponent implements OnInit, OnDestroy {
ngOnDestroy(): void {
// Clean up ESC key listener
document.removeEventListener('keydown', this.handleKeyDown);

// safety: release the lock if the component is destroyed without dismiss
// (e.g. user navigates away while modal is still open)
this.storage.set('fastFeedbackOpening', false);
}

get isRedColor(): boolean {
Expand Down
2 changes: 1 addition & 1 deletion projects/v3/src/app/components/img/img.component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, Input, isDevMode, OnChanges } from '@angular/core';
import { Component, Input, isDevMode, SimpleChanges, OnChanges } from '@angular/core';
import { getData, getAllTags } from 'exif-js';

const getImageClassToFixOrientation = (orientation) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,15 @@ <h3 class="for-accessibility" [id]="'multi-team-member-selector-question-' + que
lines="none"
*ngFor="let teamMember of question.teamMembers;let i = index"
[ngClass]="{'item-bottom-border': i !== question.teamMembers.length - 1}">
<ion-label class="white-space-normal body-2 black" [innerHTML]="teamMember.userName"></ion-label>
<ion-label class="white-space-normal body-2 black"
[innerHTML]="teamMember.userName"
[appToggleLabel]="onLabelToggle"
[toggleId]="teamMember.key"
[toggleDisabled]="control.disabled"
role="button"
tabindex="0">
</ion-label>

<ion-checkbox
[attr.aria-label]="teamMember.userName"
color="success"
Expand Down Expand Up @@ -71,7 +79,7 @@ <h3 class="for-accessibility" [id]="'multi-team-member-selector-question-' + que

<ion-checkbox color="success"
[attr.aria-label]="teamMember.userName"
[checked]="review?.answer?.includes(teamMember.key)"
[checked]="isSelectedInReview(teamMember)"
[value]="teamMember.key"
justify="start"
slot="start"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ <h3 class="for-accessibility" [id]="'multiple-choice-question-' + question.id">{
<ion-chip i18n
class="label orange"
*ngIf="submission?.answer?.includes(choice.id)"
>Learner answer</ion-chip>
>Learner's answer</ion-chip>
<ion-chip i18n
class="label yellow black"
*ngIf="review?.answer?.includes(choice.id)"
>Reviewer answer</ion-chip>
>Expert's answer</ion-chip>
</ion-label>
</ion-item>
<ng-container *ngIf="choice.explanation && choice.explanation.changingThisBreaksApplicationSecurity && submission?.answer?.includes(choice.id)">
Expand Down
8 changes: 5 additions & 3 deletions projects/v3/src/app/components/oneof/oneof.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
<div [ngClass]="{'explanation': submission?.answer === choice.id || review?.answer === choice.id}" [innerHTML]="choice.name | detectLanguage"></div>
<ion-chip class="label orange"
*ngIf="submission?.answer === choice.id" i18n
>Learner answer</ion-chip>
>Learner's answer</ion-chip>
<ion-chip class="label yellow black"
*ngIf="review?.answer === choice.id" i18n
>Reviewer answer</ion-chip>
>Expert's answer</ion-chip>
</ion-label>
</ion-item>
<ng-container *ngIf="choice?.explanation && choice?.explanation?.changingThisBreaksApplicationSecurity && submission?.answer === choice.id">
Expand Down Expand Up @@ -51,7 +51,9 @@
<ion-item lines="none"
[ngClass]="{'item-bottom-border': i !== question.choices.length - 1 || !checkInnerValue(choice.id)}">
<ion-radio color="success"
[ariaLabel]="choice.name"
justify="start"
labelPlacement="end"
[attr.aria-label]="choice.name"
[value]="choice.id"
[disabled]="control.disabled"
slot="start"
Expand Down
Loading