diff --git a/.github/workflows/p2-aus-appv3.yml b/.github/workflows/p2-aus-appv3.yml index 025195015..6dd162ba4 100644 --- a/.github/workflows/p2-aus-appv3.yml +++ b/.github/workflows/p2-aus-appv3.yml @@ -177,7 +177,8 @@ jobs: export CUSTOM_BADGE_PROJECT_URL=https://badge.aus.practera.com export CUSTOM_UPLOAD_TUS_ENDPOINT=https://tusd.practera.com/uploads/ export CUSTOM_UPLOAD_MAX_FILE_SIZE=2147483648 - export CUSTOM_HELPLINE=programs@practera.com + export CUSTOM_HELPLINE=help@practera.com + export CUSTOM_ENABLE_ASSESSMENT_PAGINATION=false export CUSTOM_STACK_NAME=${{ env.STACK_NAME }} printf "Angular environment variable creation complete\n\n" diff --git a/.github/workflows/p2-euk-appv3.yml b/.github/workflows/p2-euk-appv3.yml index 165cc8041..ea3ac7dcc 100644 --- a/.github/workflows/p2-euk-appv3.yml +++ b/.github/workflows/p2-euk-appv3.yml @@ -177,7 +177,8 @@ jobs: export CUSTOM_BADGE_PROJECT_URL=https://badge.euk.practera.com export CUSTOM_UPLOAD_TUS_ENDPOINT=https://tusd.practera.com/uploads/ export CUSTOM_UPLOAD_MAX_FILE_SIZE=2147483648 - export CUSTOM_HELPLINE=programs@practera.com + export CUSTOM_HELPLINE=help@practera.com + export CUSTOM_ENABLE_ASSESSMENT_PAGINATION=false export CUSTOM_STACK_NAME=${{ env.STACK_NAME }} printf "Angular environment variable creation complete\n\n" diff --git a/.github/workflows/p2-prerelease-appv3.yml b/.github/workflows/p2-prerelease-appv3.yml index 462e8eeb6..1419d21a9 100644 --- a/.github/workflows/p2-prerelease-appv3.yml +++ b/.github/workflows/p2-prerelease-appv3.yml @@ -180,7 +180,8 @@ jobs: export CUSTOM_BADGE_PROJECT_URL=https://badge.p2-prerelease.practera.com export CUSTOM_UPLOAD_TUS_ENDPOINT=https://tusd.practera.com/uploads/ export CUSTOM_UPLOAD_MAX_FILE_SIZE=2147483648 - export CUSTOM_HELPLINE=programs@practera.com + export CUSTOM_HELPLINE=help@practera.com + export CUSTOM_ENABLE_ASSESSMENT_PAGINATION=true export CUSTOM_STACK_NAME=${{ env.STACK_NAME }} printf "Angular environment variable creation complete\n\n" diff --git a/.github/workflows/p2-stage-appv3.yml b/.github/workflows/p2-stage-appv3.yml index 8f65bc3de..fd85793c1 100644 --- a/.github/workflows/p2-stage-appv3.yml +++ b/.github/workflows/p2-stage-appv3.yml @@ -180,7 +180,8 @@ jobs: export CUSTOM_INTERCOM=$(aws secretsmanager get-secret-value --secret-id $STACK_NAME-IntercomSecret-$ENV| jq --raw-output '.SecretString' | jq -r .app_id) export CUSTOM_BADGE_PROJECT_URL=https://badge.p2-stage.practera.com export CUSTOM_UPLOAD_TUS_ENDPOINT=https://tusd.practera.com/uploads/ - export CUSTOM_HELPLINE=programs@practera.com + export CUSTOM_HELPLINE=help@practera.com + export CUSTOM_ENABLE_ASSESSMENT_PAGINATION=true export CUSTOM_STACK_NAME=${{ env.STACK_NAME }} printf "Angular environment variable creation complete\n\n" diff --git a/.github/workflows/p2-usa-appv3.yml b/.github/workflows/p2-usa-appv3.yml index cf9f952d2..5baa65d66 100644 --- a/.github/workflows/p2-usa-appv3.yml +++ b/.github/workflows/p2-usa-appv3.yml @@ -177,7 +177,8 @@ jobs: export CUSTOM_BADGE_PROJECT_URL=https://badge.usa.practera.com export CUSTOM_UPLOAD_TUS_ENDPOINT=https://tusd.practera.com/uploads/ export CUSTOM_UPLOAD_MAX_FILE_SIZE=2147483648 - export CUSTOM_HELPLINE=programs@practera.com + export CUSTOM_HELPLINE=help@practera.com + export CUSTOM_ENABLE_ASSESSMENT_PAGINATION=false export CUSTOM_STACK_NAME=${{ env.STACK_NAME }} printf "Angular environment variable creation complete\n\n" diff --git a/docs/assessment-btndisabled-flow.md b/docs/assessment-btndisabled-flow.md new file mode 100644 index 000000000..dde8195d4 --- /dev/null +++ b/docs/assessment-btndisabled-flow.md @@ -0,0 +1,163 @@ +═══════════════════════════════════════════════════════════════════════════════════════ + btnDisabled$ BehaviorSubject Flow Diagram +═══════════════════════════════════════════════════════════════════════════════════════ + +┌─────────────────────────────────────┐ +│ activity-desktop.page.ts │ +│ (Parent Component) │ +└─────────────────────────────────────┘ + │ + │ Creates & Passes btnDisabled$ + │ as @Input to assessment.component + ▼ +┌─────────────────────────────────────┐ +│ assessment.component.ts │ +│ (Child Component) │ +│ │ +│ @Input() btnDisabled$: │ +│ BehaviorSubject │ +└─────────────────────────────────────┘ + +═══════════════════════════════════════════════════════════════════════════════════════ + TRIGGER POINTS IN assessment.component.ts +═══════════════════════════════════════════════════════════════════════════════════════ + +1. ngOnChanges() - Component Lifecycle + └── btnDisabled$.next(false) ──────────► RESET on assessment change + +2. _populateQuestionsForm() - Form Setup + ├── If no questions exist: + │ └── btnDisabled$.next(true) ───────► DISABLE (empty form) + │ + └── questionsForm.valueChanges.subscribe() + └── setSubmissionDisabled() ───────► CHECK & UPDATE based on validation + +3. _handleSubmissionData() - Submission State Handler + └── If submission.isLocked: + └── btnDisabled$.next(true) ───────► DISABLE (locked by another user) + +4. _handleReviewData() - Review State Handler + └── If isPendingReview && review.status === 'in progress': + └── btnDisabled$.next(false) ──────► ENABLE for review + +5. continueToNextTask() - Submit Action + └── If _btnAction === 'submit': + └── btnDisabled$.next(true) ───────► DISABLE during submission + +6. _submitAnswer() - Answer Submission + └── If required questions missing: + └── btnDisabled$.next(false) ──────► RE-ENABLE after validation fail + +7. resubmit() - Resubmission Flow + ├── Start: btnDisabled$.next(true) ────► DISABLE during resubmit + └── End: btnDisabled$.next(false) ─────► RE-ENABLE after completion + +8. setSubmissionDisabled() - Main Validation Logic + ├── Only runs if (doAssessment || isPendingReview) + ├── If form invalid & not disabled: + │ └── btnDisabled$.next(true) ───────► DISABLE + └── If form valid & disabled: + └── btnDisabled$.next(false) ──────► ENABLE + +9. _prefillForm() - Form Population + ├── After populating form with answers + ├── questionsForm.updateValueAndValidity() + └── If edit mode (doAssessment || isPendingReview): + └── setSubmissionDisabled() ───────► CHECK & UPDATE validation + └── If read-only mode: + └── btnDisabled$.next(false) ──────► ENSURE enabled + +10. Page Navigation Methods + ├── goToPage() + ├── nextPage() + └── prevPage() + └── setSubmissionDisabled() ──────► CHECK & UPDATE for new page + +═══════════════════════════════════════════════════════════════════════════════════════ + TRIGGER CONDITIONS SUMMARY +═══════════════════════════════════════════════════════════════════════════════════════ + +DISABLE CONDITIONS (btnDisabled$.next(true)): +├── No questions in assessment +├── Assessment is locked by another user +├── Form is invalid (required fields empty) +├── During submission process +└── During resubmit process + +ENABLE CONDITIONS (btnDisabled$.next(false)): +├── Assessment changes (reset) +├── Form becomes valid +├── Review in progress +├── After failed validation alert +├── After resubmit completion +└── Read-only mode (not doAssessment && not isPendingReview) + +═══════════════════════════════════════════════════════════════════════════════════════ + PROBLEM SCENARIO +═══════════════════════════════════════════════════════════════════════════════════════ + +User Flow - Original Issue (RESOLVED): +1. User visits Assessment A (has required fields) + └── Form invalid → btnDisabled$.next(true) ✓ + +2. User navigates to Assessment B via activity-desktop + └── ngOnChanges() → btnDisabled$.next(false) ✓ + └── _populateQuestionsForm() → questionsForm created + └── _populateFormWithAnswers() → form populated + └── setSubmissionDisabled() → checks validation + └── BUT: Timing issue - form may not be fully populated + └── Result: btnDisabled$ may remain false even if invalid + +3. RESOLVED: State synchronization fixed + └── _prefillForm() now properly checks validation after form population + +═══════════════════════════════════════════════════════════════════════════════════════ + SOLUTION IMPLEMENTATION (COMPLETED) +═══════════════════════════════════════════════════════════════════════════════════════ + +IMPLEMENTED FIXES: +1. ✅ Reset state in ngOnChanges when assessment changes + └── btnDisabled$.next(false) in ngOnChanges() + +2. ✅ Proper validation after form population in _prefillForm() + ├── questionsForm.updateValueAndValidity() + ├── Edit mode: setSubmissionDisabled() checks validation + └── Read-only mode: btnDisabled$.next(false) ensures enabled + +3. ✅ Check validation when changing pages + ├── prevPage() → setSubmissionDisabled() + ├── nextPage() → setSubmissionDisabled() + └── goToPage() → setSubmissionDisabled() + +4. ✅ Apply validation rules only when in edit mode + └── setSubmissionDisabled() has guard: (!doAssessment && !isPendingReview) + +5. ✅ Replaced _populateFormWithAnswers() with _prefillForm() + └── Better state management and validation synchronization + +RESULT: btnDisabled$ now accurately reflects form state at all times +═══════════════════════════════════════════════════════════════════════════════════════ + +═══════════════════════════════════════════════════════════════════════════════════════ + KEY LEARNINGS +═══════════════════════════════════════════════════════════════════════════════════════ + +1. BEHAVIORSUBJECT STATE PERSISTENCE + └── BehaviorSubject remembers last value across component input changes + └── Must explicitly reset when navigating between assessments + +2. FORM VALIDATION TIMING + └── Validation must happen AFTER form population is complete + └── updateValueAndValidity() is crucial for proper validation state + +3. SEPARATION OF CONCERNS + └── setSubmissionDisabled() handles validation-based enabling/disabling + └── _prefillForm() handles initial state setup after population + └── Each method has clear responsibility boundaries + +4. EDIT VS READ-ONLY MODES + └── Only apply validation rules when user can edit + └── Read-only mode should always have enabled button for navigation + └── Guard clauses prevent unnecessary state changes + +═══════════════════════════════════════════════════════════════════════════════════════ \ No newline at end of file diff --git a/docs/assessment-flow.md b/docs/assessment-flow.md new file mode 100644 index 000000000..6a267aeaf --- /dev/null +++ b/docs/assessment-flow.md @@ -0,0 +1,764 @@ +# Assessment Flow Documentation + +## Overview + +This document provides a comprehensive overview of how the Practera AppV2 assessment system works, covering the flow from activity pages through assessment components to form validation and submission handling for both learners and reviewers. + +## Architecture Overview + +The assessment system follows a hierarchical component structure with clear separation of concerns: + +``` +Activity Pages (Desktop/Mobile) + ↓ +Assessment Component (Central Hub) + ↓ (with Pagination enabled) +Page Indicators ←→ Question Groups (Split into Pages) ←→ Navigation Controls + ↓ +Question Components (Text, File, Multiple Choice, etc.) + ↓ +Bottom Action Bar (Submit/Continue Button + Pagination Controls) +``` + +### Pagination Flow +When pagination is enabled (`environment.featureToggles.assessmentPagination = true` - environment variable file): + +``` +1. Assessment loads → splitGroupsByQuestionCount() +2. Groups divided into pages (≤8 questions per page) +3. Page indicators show completion status +4. Users navigate: Prev/Next buttons or click page indicators +5. Form validation tracks completion per page +6. Submit button integrates with pagination controls +``` + +## Core Components + +### 1. Entry Point Pages + +#### Activity Desktop Page (`activity-desktop.page.ts`) +- **Purpose**: Main desktop interface for learners doing assessments +- **Key Responsibilities**: + - Manages activity and task navigation + - Handles assessment loading and submission + - Controls button states and loading indicators + - Coordinates with activity service for task progression + +**Key Properties:** +```typescript +assessment = this.assessmentService.assessment$; +submission: Submission; +review: AssessmentReview; +savingText$: BehaviorSubject = new BehaviorSubject(''); +btnDisabled$: BehaviorSubject = new BehaviorSubject(false); +``` + +**Assessment Flow:** +1. Loads assessment via `assessmentService.getAssessment()` +2. Displays assessment component with current data +3. Handles save events from assessment component +4. Manages button states during submission + +#### Assessment Mobile Page (`assessment-mobile.page.ts`) +- **Purpose**: Mobile-optimized interface for assessments +- **Similar functionality** to desktop but adapted for mobile UX +- **Key Differences**: + - Mobile-specific navigation patterns + - Touch-optimized interactions + - Responsive layout adjustments + +#### Review Desktop Page (`review-desktop.page.ts`) +- **Purpose**: Interface for reviewers to evaluate learner submissions +- **Key Responsibilities**: + - Manages review list and current review selection + - Handles review submission and feedback + - Coordinates between review list and assessment components + +**Review-Specific Properties:** +```typescript +currentReview: Review; +reviews: Review[]; +noReview: boolean = false; +``` + +### 2. Central Assessment Component (`assessment.component.ts`) + +The assessment component is the central hub that orchestrates the entire assessment experience for both learners and reviewers. + +#### Core State Management + +**Action Types:** +- `'assessment'` - Learner doing assessment or viewing feedback +- `'review'` - Reviewer providing feedback on learner submission + +**State Flags:** +```typescript +doAssessment: boolean = false; // Learner can edit assessment +isPendingReview: boolean = false; // Reviewer can edit review +feedbackReviewed: boolean = false; // Learner has seen feedback +``` + +**Form Management:** +```typescript +questionsForm: FormGroup = new FormGroup({}); +// Form controls named 'q-{questionId}' for dynamic question handling +``` + +#### Data Flow Logic + +**For Learners (`action === 'assessment'`):** +1. **Not Started/In Progress**: `doAssessment = true` + - Form controls are editable + - Required validators applied to learner-audience questions + - Auto-save functionality enabled + - Submit button available + +2. **Pending Review**: Read-only mode + - Form controls disabled + - Show "waiting for review" message + - No submit functionality + +3. **Feedback Available**: Read-only with feedback + - Display learner answers and reviewer feedback + - "Mark as Read" button to acknowledge feedback + - Navigation to next task after reading + +**For Reviewers (`action === 'review'`):** +1. **Pending Review**: `isPendingReview = true` + - Display learner submission as reference (read-only) + - Reviewer form controls are editable + - Required validators applied to reviewer-audience questions + - "Submit Review" button available + +2. **Review Complete**: Read-only mode + - Show completed review + - No further editing allowed + +#### Form Population Logic + +The form population has been refactored to ensure proper timing and validation state management. + +**Assessment Answers (`this.action === 'assessment'`):** +```typescript +private _prefillForm(): void { + // populate form with submission answers (for assessment action) + if (this.submission?.answers && this.action === 'assessment') { + Object.keys(this.submission.answers).forEach(questionId => { + const controlName = 'q-' + questionId; + const control = this.questionsForm.get(controlName); + if (control && this.submission.answers[questionId]?.answer !== undefined) { + control.setValue(this.submission.answers[questionId].answer, { emitEvent: false }); + } + }); + } + + // populate form with review answers (for review action) + if (this.review?.answers && this.action === 'review') { + Object.keys(this.review.answers).forEach(questionId => { + const controlName = 'q-' + questionId; + const control = this.questionsForm.get(controlName); + if (control && this.review.answers[questionId]) { + const reviewAnswer = { + answer: this.review.answers[questionId].answer, + comment: this.review.answers[questionId].comment, + file: this.review.answers[questionId].file || null, + }; + control.setValue(reviewAnswer, { emitEvent: false }); + } + }); + } + + // revalidate form after setting values + this.questionsForm.updateValueAndValidity(); + + // check validation state and update button accordingly + if (this.doAssessment || this.isPendingReview) { + // in edit mode, check form validation + this.setSubmissionDisabled(); + } else { + // in read-only mode, ensure button is enabled + this.btnDisabled$.next(false); + } +} +``` + +#### Required Field Validation + +**Validation Rules:** +- Required validators only applied when user can edit +- `doAssessment = true`: Learner doing assessment +- `isPendingReview = true`: Reviewer doing review +- Read-only modes have no required validation + +**Implementation:** +```typescript +private _populateQuestionsForm() { + this.assessment.groups.forEach(group => { + group.questions.forEach(question => { + let validator = []; + + // Apply required validator based on user role and edit permissions + if (this._isRequired(question) === true) { + validator = [Validators.required]; + } + + this.questionsForm.addControl('q-' + question.id, new FormControl('', validator)); + }); + }); + + // Update button state based on form validity + this.questionsForm.valueChanges.pipe( + takeUntil(this.unsubscribe$), + debounceTime(300), + ).subscribe(() => { + this.btnDisabled$.next(this.questionsForm.invalid); + }); +} + +private _isRequired(question: Question): boolean { + if (!question.isRequired) return false; + + // Check if current user can edit and question applies to them + if (this.doAssessment && question.audience.includes('submitter')) { + return true; + } + + if (this.isPendingReview && question.audience.includes('reviewer')) { + return true; + } + + return false; +} +``` + +### 3. Question Components + +Each question type has its dedicated component that handles specific input requirements: + +#### File Upload Component (`app-file-upload`) +**Dual Purpose Display:** +- **Learner View**: Shows upload interface or uploaded file +- **Reviewer View**: Shows learner's file + reviewer's file upload interface + +**Key Properties:** +```typescript +[question]="question" // Question metadata +[doAssessment]="doAssessment" // Learner can edit +[doReview]="isPendingReview" // Reviewer can edit +[submission]="submission?.answers[question.id] || {}" // Learner's answer +[review]="review?.answers[question.id] || {}" // Reviewer's answer +[control]="questionsForm?.controls['q-' + question.id]" // Form control +``` + +**Form Control Updates:** +```typescript +onFileUploaded(file: any) { + if (this.doReview && this.control) { + this.control.setValue(file); + this.control.markAsTouched(); + this.control.updateValueAndValidity(); + } +} + +onFileRemoved() { + if (this.doReview && this.control) { + this.control.setValue(null); + this.control.markAsTouched(); + this.control.updateValueAndValidity(); + } +} +``` + +#### Other Question Components +- **Text Component** (`app-text`): Text input with rich text support +- **Multiple Component** (`app-multiple`): Multiple choice questions +- **Oneof Component** (`app-oneof`): Single choice questions +- **Team Member Selector** (`app-team-member-selector`): Team member selection + +All follow similar patterns with dual-purpose display for learner/reviewer contexts. + +### 4. Bottom Action Bar (`bottom-action-bar.component.html`) + +**Purpose**: Provides the primary action button for assessment submission/continuation + +**Key Features:** +```html +{{ text }} +``` + +**Button States:** +- **Enabled**: Form is valid and user can submit +- **Disabled**: Form has validation errors or submission in progress +- **Dynamic Text**: Changes based on context (Submit, Continue, Mark as Read, etc.) + +## Data Flow Diagrams + +### Assessment Submission Flow (Learner) + +``` +1. Activity Desktop Page + ↓ (Load Assessment) +2. AssessmentService.fetchAssessment() + ↓ (GraphQL Query) +3. Assessment Component receives data + ↓ (Initialize form) +4. Question Components populate with answers + ↓ (User interaction) +5. Form validation triggers + ↓ (Valid form) +6. Bottom Action Bar enabled + ↓ (User clicks submit) +7. Assessment Component emits save event + ↓ (Handle save) +8. Activity Desktop Page calls AssessmentService.submitAssessment() + ↓ (API call) +9. Success: Navigate to next task +``` + +### Review Submission Flow (Reviewer) + +``` +1. Review Desktop Page + ↓ (Select Review) +2. AssessmentService.fetchAssessment() with action='review' + ↓ (GraphQL Query with reviewer=true) +3. Assessment Component receives: + - Assessment structure + - Learner submission (reference) + - Review data (editable) + ↓ (Initialize form) +4. Question Components show: + - Learner answers (read-only) + - Reviewer input fields (editable) + ↓ (Reviewer interaction) +5. Form validation for reviewer fields + ↓ (Valid review form) +6. Bottom Action Bar enabled + ↓ (Reviewer clicks submit) +7. Assessment Component emits save event + ↓ (Handle review save) +8. Review Desktop Page calls AssessmentService.submitReview() + ↓ (API call) +9. Success: Update review list +``` + +## Form Validation System + +### Validation Rules + +**Required Field Logic:** +```typescript +// Only apply required validation when user can edit +if (this._isRequired(question) === true) { + validator = [Validators.required]; +} + +private _isRequired(question: Question): boolean { + if (!question.isRequired) return false; + + // Learner doing assessment + if (this.doAssessment && question.audience.includes('submitter')) { + return true; + } + + // Reviewer doing review + if (this.isPendingReview && question.audience.includes('reviewer')) { + return true; + } + + return false; +} +``` + +**Button State Management:** +```typescript +// Delayed subscription to avoid race conditions during initialization +setTimeout(() => { + this.questionsForm.valueChanges.pipe( + takeUntil(this.unsubscribe$), + debounceTime(300), + ).subscribe(() => { + this.initializePageCompletion(); + this.setSubmissionDisabled(); + }); +}, 300); + +setSubmissionDisabled() { + // only enforce form validation when user can actually edit + if (!this.doAssessment && !this.isPendingReview) { + return; + } + + this.btnDisabled$.next(this.questionsForm.invalid); +} +``` + +### Validation Flow for Required File Questions + +**Scenario**: Reviewer must upload a file for a required question + +1. **Form Setup**: + ```typescript + // question.isRequired = true, question.audience = ['reviewer'] + // isPendingReview = true (reviewer can edit) + const validator = [Validators.required]; + this.questionsForm.addControl('q-' + question.id, new FormControl('', validator)); + ``` + +2. **Initial State**: + - Form control value: `null` or `''` + - Form validity: `invalid` + - Button state: `disabled` + +3. **File Upload**: + ```typescript + // File upload component updates control + this.control.setValue(fileObject); + this.control.updateValueAndValidity(); + ``` + +4. **Validation Update**: + - Form control value: `fileObject` + - Form validity: `valid` + - Button state: `enabled` + +## Assessment Pagination System + +### Core Properties +```typescript +pageSize = 8; // Maximum questions per page +pageIndex: number = 0; // Current page (0-based) +pagesGroups: any[] = []; // Pages containing question groups +pageRequiredCompletion: boolean[] = []; // Completion status per page +readonly manyPages = 6; // Minimum pages for scrollable pagination +``` + +### Page Generation Logic +```typescript +splitGroupsByQuestionCount() { + // Divides assessment groups into pages + // - Multiple small groups can fit on one page if total questions ≤ pageSize + // - Large groups with >pageSize questions are split across multiple pages + // - Preserves group structure where possible +} +``` + +### Navigation Methods +```typescript +prevPage() // Go to previous page with boundary check +nextPage() // Go to next page with boundary check +goToPage(i: number) // Jump to specific page with validation +``` + +### Completion Tracking + +The completion tracking system has been improved to handle proper initialization timing and avoid the "incompleted" class showing incorrectly on first load. + +```typescript +ngOnChanges(changes: SimpleChanges): void { + if (!this.assessment) { + return; + } + + this._initialise(); + + if (changes.assessment || changes.submission || changes.review) { + // reset button state when assessment changes + this.btnDisabled$.next(false); + this.pageRequiredCompletion = []; + + this._handleSubmissionData(); + this._populateQuestionsForm(); + this._handleReviewData(); + this._prefillForm(); + } + + // split by question count every time assessment changes - only if pagination is enabled + if (this.isPaginationEnabled) { + this.pagesGroups = this.splitGroupsByQuestionCount(); + this.pageIndex = 0; + + // initialize page completion after form is fully set up + // use delay to ensure form values are populated + setTimeout(() => { + this.initializePageCompletion(); + }, 200); + } else { + // Reset pagination data when disabled + this.pagesGroups = []; + this.pageIndex = 0; + } + + // scroll to the active page into view after rendering + setTimeout(() => this.scrollActivePageIntoView(), 250); +} + +initializePageCompletion() { + if (!this.isPaginationEnabled) return; + + // Only track completion status when user can actually edit the form + // In read-only mode (viewing feedback or completed submissions), completion tracking is not relevant + if (!this.doAssessment && !this.isPendingReview) { + // Set all pages as completed for read-only mode to avoid showing incomplete indicators + this.pageRequiredCompletion = new Array(this.pageCount).fill(true); + this.cdr.detectChanges(); + setTimeout(() => this.scrollActivePageIntoView(), 100); + return; + } + + this.pageRequiredCompletion = new Array(this.pageCount).fill(true); + + this.pages.forEach((page, index) => { + const pageQuestions = this.getAllQuestionsForPage(index); + this.pageRequiredCompletion[index] = this.areAllRequiredQuestionsAnswered(pageQuestions); + }); + + // trigger change detection to update the view + this.cdr.detectChanges(); + + // Update the scroll position when page completion status changes + setTimeout(() => this.scrollActivePageIntoView(), 100); +} +``` + +**Key Improvements for First Load Issue:** +1. **Timing Fix**: `initializePageCompletion()` is now called in `ngOnChanges()` after pagination setup with a 200ms delay +2. **Change Detection**: Added `this.cdr.detectChanges()` to ensure the view updates when completion status changes +3. **Form Population Order**: Form values are populated via `_prefillForm()` before completion tracking runs +4. **Race Condition Prevention**: Delayed form valueChanges subscription to avoid interference during initialization +5. **Read-Only Mode Handling**: Completion tracking is disabled when users are viewing feedback or completed submissions (`!doAssessment && !isPendingReview`), showing all pages as completed instead + +## Pagination Issue Fixes + +### Problem: "Incompleted" Class on First Load + +**Issue Description:** +Page indicators showed up with the "incompleted" class on first load of assessments, even when questions were already answered. This occurred due to a timing mismatch between form population and completion tracking initialization. + +**Root Cause:** +The `initializePageCompletion()` method was being called before form values were fully populated, causing `areAllRequiredQuestionsAnswered()` to return false for completed questions. + +**Solution Implemented:** + +1. **Moved completion initialization to proper lifecycle hook:** + ```typescript + // In ngOnChanges(), after pagination setup + setTimeout(() => { + this.initializePageCompletion(); + }, 200); + ``` + +2. **Added change detection trigger:** + ```typescript + initializePageCompletion() { + // ... completion logic ... + this.cdr.detectChanges(); // Ensure view updates + } + ``` + +3. **Separated form population logic:** + ```typescript + private _prefillForm(): void { + // Form population with proper validation state management + // Called before completion tracking + } + ``` + +4. **Delayed form valueChanges subscription:** + ```typescript + setTimeout(() => { + this.questionsForm.valueChanges.pipe( + takeUntil(this.unsubscribe$), + debounceTime(300), + ).subscribe(() => { + this.initializePageCompletion(); + this.setSubmissionDisabled(); + }); + }, 300); + ``` + +**Result:** +Page indicators now correctly show completion status on first load, with proper visual feedback for answered and unanswered required questions. + +## Error Handling + +### Validation Errors +- Form validation prevents submission when required fields are empty +- Visual indicators show which fields need attention +- Real-time validation feedback as user types/selects + +### Network Errors +- Auto-save failures trigger retry mechanism +- Submission failures show error messages +- Automatic logout if JWT token expires + +### File Upload Errors +- Upload failures with retry mechanisms +- File size and type validation +- Progress indicators during upload + +## Security Considerations + +### Role-Based Access +- Questions can specify audience: `['submitter', 'reviewer']` +- Form controls only editable based on user role and submission status +- API validates user permissions before allowing actions + +### Data Integrity +- Form validation ensures required fields are completed +- Server-side validation confirms data integrity +- Optimistic updates with rollback on failure + +## Performance Optimizations + +### Change Detection +```typescript +changeDetection: ChangeDetectionStrategy.OnPush +``` + +### Observable Management +```typescript +takeUntil(this.unsubscribe$) // Prevent memory leaks +debounceTime(300) // Reduce validation frequency +shareReplay(1) // Cache service responses +``` + +### Lazy Loading +- Question components loaded on demand +- Assessment data fetched when needed +- Pagination reduces DOM complexity + +## Troubleshooting + +### Common Pagination Issues + +1. **Page indicators show as incomplete on first load:** + - **Cause**: `initializePageCompletion()` called before form values are set + - **Solution**: Ensure proper timing in `ngOnChanges()` with delays + +2. **Form validation not working correctly:** + - **Cause**: Race condition between form population and validation setup + - **Solution**: Use `_prefillForm()` method with proper sequencing + +3. **Change detection not triggering:** + - **Cause**: OnPush change detection strategy requires manual triggering + - **Solution**: Call `this.cdr.detectChanges()` after completion updates + +4. **Button state incorrect on load:** + - **Cause**: Button state set before form is properly initialized + - **Solution**: Use `setSubmissionDisabled()` method with proper conditions + +5. **Completion indicators showing in read-only mode:** + - **Cause**: Completion tracking running when user is viewing feedback/completed submissions + - **Solution**: Check `doAssessment` and `isPendingReview` flags before running completion logic + + +## Testing Considerations + +### Unit Tests +- Mock assessment service responses +- Test form validation logic +- Verify button state changes +- Component interaction testing +- Test pagination initialization timing + +### Integration Tests +- End-to-end assessment submission flow +- Review workflow testing +- File upload functionality +- Cross-browser compatibility + +### Test Scenarios +1. **Learner Assessment Flow**: + - Start new assessment + - Save progress (auto-save) + - Submit assessment + - View feedback + +2. **Reviewer Flow**: + - View learner submission + - Provide feedback + - Submit review + - Handle required fields + +3. **Edge Cases**: + - Network interruptions + - Invalid file uploads + - Session timeouts + - Concurrent submissions + +## Configuration + +### Environment Features +```typescript +environment.featureToggles.assessmentPagination // Enable/disable pagination +``` + +### Question Types +- `text` - Text input +- `file` - File upload +- `video` - Video file upload +- `oneof` - Single choice +- `multiple` - Multiple choice +- `team-member-selector` - Team member selection +- `multi-team-member-selector` - Multiple team member selection + +## API Integration + +### GraphQL Queries +```graphql +query getAssessment($assessmentId: Int!, $reviewer: Boolean!, $activityId: Int, $contextId: Int!, $submissionId: Int) { + assessment(id:$assessmentId, reviewer:$reviewer, activityId:$activityId, submissionId:$submissionId) { + id name type description dueDate isTeam pulseCheck allowResubmit + groups { + name description + questions { + id name description type isRequired hasComment audience fileType + choices { id name explanation description } + teamMembers { userId userName teamId } + } + } + submissions(contextId:$contextId) { + id status completed modified locked + submitter { name image team { name } } + answers { questionId answer file { name url type } } + review { + id status modified meta + reviewer { name } + answers { questionId answer comment file { name url type size } } + } + } + } +} +``` + +### Mutation Operations +- `submitAssessment` - Learner submission +- `submitReview` - Reviewer feedback +- `saveAnswers` - Auto-save progress + +## Conclusion + +The assessment system provides a comprehensive, role-based assessment and review platform with comprehensive form validation, real-time feedback, and optimized user experience for both learners and reviewers. The modular architecture ensures maintainability while the reactive patterns provide responsive user interactions. + +Key strengths: +- Clear separation of concerns between components +- Reactive form validation with real-time feedback +- Dual-purpose components for learner/reviewer contexts +- Robust error handling and network resilience +- Performance optimizations for large assessments +- Comprehensive pagination system for long assessments +- **Improved initialization timing** to prevent incorrect "incompleted" status on first load +- **Proper change detection management** with OnPush strategy +- **Race condition prevention** through strategic delays and sequencing + +Recent improvements have specifically addressed timing issues that could cause pagination indicators to display incorrectly on first load, ensuring a more reliable and user-friendly assessment experience. + + +## References +- [Button Disabled State Flow](assessment-btndisabled-flow.md) - btnDisabled$ BehaviorSubject flow diagram across assessment component lifecycle \ No newline at end of file diff --git a/docs/directives/toggleLabelDirective.md b/docs/directives/toggleLabelDirective.md new file mode 100644 index 000000000..4e323ec48 --- /dev/null +++ b/docs/directives/toggleLabelDirective.md @@ -0,0 +1,37 @@ +# Toggle Label Directive + +This directive was created to accommodate the use of innerHTML in ion-checkbox and ion-radio labels. It provides a solution for dynamically toggling label content and handling HTML content within Ionic form controls where standard label binding may not be sufficient. + +## Problem Statement +The issue arises when clicking on the content within the innerHTML title text of ion-checkbox or ion-radio components. The expected behavior is that clicking the label should toggle the checkbox or radio button state. However, due to the way innerHTML is handled, the click event does not propagate correctly to the underlying input element, preventing the toggle action from occurring. This can lead to a confusing user experience where the label appears clickable but does not perform the intended function. + +### Comparison of Implementations + +#### Default Recommended Code Implementation +```html + + Toggle me + +``` +In this implementation, clicking the label correctly toggles the checkbox state because the label is directly associated with the checkbox input. + +#### Implementation with innerHTML +```html + + + +``` +In this case, while the label appears clickable, the click event does not propagate to the checkbox input, resulting in no toggle action occurring when the label is clicked. + +This comparison highlights the importance of using standard label binding to ensure proper functionality in Ionic form controls. + +## Purpose +- Enable dynamic label content for Ionic checkbox and radio components +- Support HTML content rendering within form control labels +- Provide consistent label behavior across different form input types + +## Usage +Apply this directive to elements that need dynamic label toggling functionality, particularly useful with Ionic form controls that require innerHTML support. + +## Note +This directive addresses limitations in standard Ionic label handling where innerHTML content needs to be dynamically managed for checkbox and radio controls. diff --git a/docs/docs.md b/docs/docs.md index 598cc275f..585bbe02d 100644 --- a/docs/docs.md +++ b/docs/docs.md @@ -7,4 +7,7 @@ This is practera documentation with more informations. ### Components - [Chat Room Component](./components/chatRoomComponent.md) -- [Chat List Component](./components/chatListComponent.md) \ No newline at end of file +- [Chat List Component](./components/chatListComponent.md) + +### Directives +- [Toggle Label Directive](./directives/toggleLabelDirective.md) \ No newline at end of file diff --git a/docs/features/assessment-pagination-feature-toggle.md b/docs/features/assessment-pagination-feature-toggle.md new file mode 100644 index 000000000..c475c43bc --- /dev/null +++ b/docs/features/assessment-pagination-feature-toggle.md @@ -0,0 +1,86 @@ +# Assessment Pagination Feature Toggle + +This document explains how to enable/disable the assessment pagination feature using environment flags. + +## Configuration + +The pagination feature is controlled by the `featureToggles.assessmentPagination` flag in the environment files. + +### Environment Files + +The feature toggle is configured in the following files: +- `src/environments/environment.ts` (staging/default) +- `src/environments/environment.prod.ts` (production) +- `src/environments/environment.local.ts` (local development) +- `src/environments/environment.custom.ts` (custom environments) + +### Flag Configuration + +```typescript +export const environment = { + // ... other environment properties + featureToggles: { + assessmentPagination: true, // Set to false to disable pagination + }, +}; +``` + +## Behavior + +### When `assessmentPagination: true` (Default) +- Assessment questions are split across multiple pages (8 questions per page by default) +- Pagination controls (Previous/Next buttons and page indicators) are visible +- Users can navigate between pages using buttons or clicking page indicators +- Page completion indicators show which pages have unanswered required questions + +### When `assessmentPagination: false` +- All assessment questions are displayed on a single page +- No pagination controls are shown +- Traditional single-page assessment experience +- All questions are accessible without navigation + +## Technical Implementation + +The feature toggle affects: + +1. **Template Rendering**: Pagination UI is conditionally rendered based on `isPaginationEnabled` +2. **Question Display**: Questions are either paginated or shown all at once via `pagedGroups` getter +3. **Navigation Methods**: Pagination methods (`prevPage`, `nextPage`, etc.) are safe-guarded +4. **Page Completion**: Completion tracking is only active when pagination is enabled + +## Usage Examples + +### Disable pagination for a specific environment: +```typescript +// environment.local.ts +export const environment = { + // ... other properties + featureToggles: { + assessmentPagination: false, // Single page mode + }, +}; +``` + +### Enable pagination (default behavior): +```typescript +// environment.prod.ts +export const environment = { + // ... other properties + featureToggles: { + assessmentPagination: true, // Multi-page mode + }, +}; +``` + +## Testing + +To test the feature toggle: + +1. Modify the `assessmentPagination` flag in your target environment file +2. Rebuild the application with the appropriate environment configuration +3. Navigate to any assessment +4. Verify the pagination behavior matches the configuration + +## Backward Compatibility + +The feature toggle defaults to `true` (pagination enabled) if not explicitly set, ensuring backward compatibility with existing deployments. diff --git a/docs/fixes/CORE-8166-8167-pagination-answer-persistence.md b/docs/fixes/CORE-8166-8167-pagination-answer-persistence.md new file mode 100644 index 000000000..4e2638db3 --- /dev/null +++ b/docs/fixes/CORE-8166-8167-pagination-answer-persistence.md @@ -0,0 +1,286 @@ +# CORE-8166 / CORE-8167 — Pagination Answer Persistence Fix + +> **Branch:** `2.4.8/CORE-8166/review-missing-local-answer` and `2.4.8/CORE-8167/standalone-pagination-completed-review-trunk` +> **PR:** [#2636](https://github.com/intersective/app/pull/2636) +> **Date:** March 2026 + +--- + +## Problem Summary + +When an assessment uses **pagination** (more than 8 questions, split across pages), navigating between pages causes previously entered answers to be **cleared from the UI**. The user's selections (checkboxes, radio buttons, text, etc.) disappear when they leave a page and return to it. + +This affects both: +- **Reviewer mode** (`doReview`) — reviewer's in-progress answers lost on page navigation +- **Assessment mode** (`doAssessment`) — learner's in-progress answers lost on page navigation (specific to `multi-team-member-selector`) + +--- + +## Root Cause Analysis + +### How Pagination Works + +Pagination splits assessment groups into pages via `pagesGroups` array. The template renders only the current page's questions using: + +```html + +``` + +Where `pagedGroups` returns `this.pagesGroups[this.pageIndex]`. + +**When the user navigates to a different page, Angular destroys the question components on the current page and creates new ones for the target page.** When navigating back, fresh component instances are created — they run `ngOnInit()` and `_showSavedAnswers()` again. + +The `FormGroup` (`questionsForm`) **persists** across page changes — only the visual components are destroyed/recreated. Form controls retain their values. + +### The ControlValueAccessor Lifecycle + +Each question component implements `ControlValueAccessor`. The lifecycle on component creation is: + +1. Component `ngOnInit()` runs → calls `_showSavedAnswers()` +2. `FormControlName` directive's `ngOnInit()` runs → calls `writeValue()` with the current form control value +3. `registerOnChange()` is called — `propagateChange()` becomes functional + +This means `propagateChange()` is a **no-op** during step 1, and `writeValue()` in step 2 is the authoritative source of the form control's current value. + +### Three Distinct Bugs + +#### Bug 1: `_showSavedAnswers()` Overwrites Dirty Form Controls + +**Affected modes:** `doReview`, `doAssessment` +**Affected types:** all question types + +When a component is recreated on pagination return, `_showSavedAnswers()` in `ngOnInit()` unconditionally read from `@Input` data (e.g., `this.review.answer`, `this.submission.answer`) — which is the **original API data**, not the user's edits. This overwrites `innerValue` with stale data. + +**Fix:** check `control.pristine` before deciding the data source: +- If `control.pristine` → use API data (no local edits exist) +- If `!control.pristine` (dirty) → use `control.value` (preserves local edits) + +```typescript +// example from oneof.component.ts +if (this.control && !this.control.pristine) { + this.innerValue = this.control.value; + this.comment = this.control.value?.comment ?? this.review.comment; +} else { + this.innerValue = { + answer: this.review.answer, + comment: this.review.comment, + }; + this.comment = this.review.comment; +} +``` + +#### Bug 2: Template Bindings Read From Stale `@Input` Data + +**Affected modes:** `doReview` +**Affected types:** `multiple`, `oneof`, `team-member-selector`, `multi-team-member-selector` + +Even after fixing `_showSavedAnswers()`, templates in review mode were binding directly to `@Input` properties (e.g., `review.answer`) instead of the local `innerValue`. So checkbox `[checked]` and radio `[value]` bindings showed the original API answers, not the user's edits. + +**Fix:** change all review-mode template bindings to use `innerValue`: + +| Component | Before | After | +|---|---|---| +| `multiple` | `review.answer.includes(choice.id)` | `innerValue?.answer?.includes(choice.id)` | +| `oneof` | `review.answer` | `innerValue?.answer` | +| `team-member-selector` | `review?.answer` | `innerValue?.answer` | +| `multi-team-member-selector` | `isSelectedInReview(teamMember)` | `isSelected(teamMember)` | + +#### Bug 3: Array Type Initialization Mismatch + +**Affected modes:** `doReview` (multiple, multi-team-member-selector), `doAssessment` (multi-team-member-selector) +**Affected types:** `multiple`, `multi-team-member-selector` + +For checkbox-based question types, the form control was initialized with `answer: ''` (empty string) instead of `answer: []` (empty array). When `writeValue()` populated `innerValue` with this string value, subsequent calls to `addOrRemove()` crashed with `TypeError: arrayInput.push is not a function` because an empty string is not an array. This silent error prevented `propagateChange()` from executing, so the form control stayed pristine with no user edits actually saved. + +**Fix (assessment.component.ts `_populateQuestionsForm()`):** +```typescript +if (this.action === 'review') { + const arrayTypes = ['multiple', 'multi team member selector']; + quesCtrl = { + comment: '', + answer: arrayTypes.includes(question.type) ? [] : '', + file: null + }; +} else { + // assessment mode: multi-team-member-selector uses a plain array + if (question.type === 'multi team member selector') { + quesCtrl = []; + } +} +``` + +**Fix (component-level guards):** added array coercion in `writeValue()`, `onChange()`, and `_showSavedAnswers()` for both `multiple` and `multi-team-member-selector`: +```typescript +// writeValue guard +if (this.doReview && this.innerValue && !Array.isArray(this.innerValue.answer)) { + this.innerValue = { ...this.innerValue, answer: [] }; +} + +// onChange guard +if (!Array.isArray(this.innerValue.answer)) { + this.innerValue.answer = []; +} +``` + +--- + +## Why `doAssessment` Mode Was Also Affected + +The `multi-team-member-selector` component was specifically affected in assessment mode because of a **data shape mismatch**: + +- In **assessment mode**, the component treats `innerValue` as a **plain array** (e.g., `['key1', 'key2']`). Methods like `onChange()`, `isSelected()`, and `triggerSave()` all operate on `innerValue` directly as an array. +- However, `_populateQuestionsForm()` initialized the form control with `null` (the default for all assessment-mode controls). +- When `writeValue(null)` was called, the null check `if (value)` skipped setting `innerValue`, leaving it undefined. +- On first checkbox click, `onChange()` called `this.utils.addOrRemove(this.innerValue, value)` — which crashed because `this.innerValue` was not an array. + +**Fix:** initialize `multi team member selector` with `[]` in assessment mode: +```typescript +if (question.type === 'multi team member selector') { + quesCtrl = []; +} +``` + +Plus a defensive guard in `writeValue()`: +```typescript +if (this.doAssessment && !Array.isArray(this.innerValue)) { + this.innerValue = Array.isArray(this.innerValue?.answer) ? this.innerValue.answer : []; +} +``` + +Other question types in assessment mode were **not affected** because: +- `multiple` already had a null guard: `if (!this.innerValue) { this.innerValue = []; }` +- Scalar types (`oneof`, `text`, `slider`, `team-member-selector`) use direct assignment, not array operations +- `file-upload` uses `fileRequestFormat()` which safely returns `{}` for null + +--- + +## Question Types Affected + +| Question Type | Component | Review Mode Fix | Assessment Mode Fix | +|---|---|---|---| +| Radio (single choice) | `app-oneof` | pristine check + template binding | — | +| Checkbox (multiple choice) | `app-multiple` | pristine check + template binding + array init | — | +| Text / Textarea | `app-text` | pristine check | — | +| Slider | `app-slider` | pristine check | — | +| File Upload | `app-file-upload` | pristine check | — | +| Team Member (single) | `app-team-member-selector` | pristine check + template binding | — | +| Team Member (multi) | `app-multi-team-member-selector` | pristine check + template binding + array init | array init + writeValue guard | + +--- + +## Files Changed + +### Parent Component +- **assessment.component.ts** — `_populateQuestionsForm()`: proper initial values for array-type controls in both review and assessment modes; consolidated `_prefillForm()` method + +### Question Components (TypeScript) +- **multiple.component.ts** — `_showSavedAnswers()`, `writeValue()`, `onChange()`: pristine check, array coercion +- **oneof.component.ts** — `_showSavedAnswers()`, `writeValue()`: pristine check, comment restoration +- **text.component.ts** — `_showSavedAnswers()`, `writeValue()`: pristine check, object-vs-string handling +- **slider.component.ts** — `_showSavedAnswers()`, `writeValue()`: pristine check, comment restoration +- **team-member-selector.component.ts** — `_showSavedAnswers()`, `writeValue()`: pristine check, comment restoration +- **multi-team-member-selector.component.ts** — `_showSavedAnswers()`, `writeValue()`, `onChange()`: pristine check, array coercion, assessment mode plain-array guard +- **file-upload.component.ts** — `_showSavedAnswers()`: pristine check + +### Question Components (Templates) +- **multiple.component.html** — `[checked]` binding: `review.answer.includes()` → `innerValue?.answer?.includes()`; `onLabelToggle` passes `'answer'` type in review mode +- **oneof.component.html** — `[value]` binding: `review.answer` → `innerValue?.answer` +- **team-member-selector.component.html** — `[value]` binding: `review?.answer` → `innerValue?.answer` +- **multi-team-member-selector.component.html** — `[checked]` binding: `isSelectedInReview()` → `isSelected()` in doReview section + +--- + +## The `control.pristine` Pattern + +All question components now follow a consistent pattern in `_showSavedAnswers()`: + +``` +┌─────────────────────────────────────────────────┐ +│ Component recreated on pagination return │ +│ │ +│ ngOnInit() → _showSavedAnswers() │ +│ │ +│ Is control.pristine? │ +│ YES → Use @Input data (API/original) │ +│ NO → Use control.value (user's local edits) │ +│ │ +│ writeValue() called by FormControlName │ +│ → Sets innerValue from form control value │ +│ → Template binds to innerValue (not @Input) │ +└─────────────────────────────────────────────────┘ +``` + +**Why `pristine` works as the discriminator:** +- `_prefillForm()` calls `control.setValue(value, { emitEvent: false })` — this does NOT mark the control as dirty (it stays pristine) +- User interactions call `propagateChange()` → which DOES mark the control as dirty +- So `pristine = true` means "only API data, no user edits" and `pristine = false` means "user has made changes" + +**Wait — `setValue()` does mark the control as dirty in some Angular versions.** Actually, `setValue()` with `{ emitEvent: false }` still changes the pristine state to false. The key insight is: +- On first load, `_prefillForm()` sets the value → `pristine = false` +- `_showSavedAnswers()` reads `control.value` which already has the prefilled value +- So either path (pristine or not) produces the correct result on first load +- On pagination return (component recreated), the form control still has the user's edits from `propagateChange()`, and `control.pristine = false`, so `_showSavedAnswers()` correctly reads from `control.value` + +--- + +## Data Shape Reference + +### Review Mode +Form control value is always an **object**: +```typescript +{ answer: any, comment: string, file?: any } +``` +- `answer` is `[]` for checkbox types, `''` or scalar for others +- Components access `innerValue.answer` and `innerValue.comment` separately + +### Assessment Mode +Form control value varies by type: +```typescript +// oneof, team-member-selector: scalar (string/number) +'choice-id' or 5 + +// multiple: array +[1, 3, 5] + +// multi-team-member-selector: array of JSON strings +['{"userId":1,"name":"..."}', '{"userId":2,"name":"..."}'] + +// text: string +'answer text' + +// slider: number +75 + +// file-upload: FileInput object +{ url: '...', name: 'file.pdf', ... } +``` + +--- + +## Three Selection Check Functions in `multi-team-member-selector` + +| Function | Data Source | Purpose | Used In Template Sections | +|---|---|---|---| +| `isSelected()` | `this.innerValue` (local state) | current working state including unsaved edits | `doAssessment`, `doReview` — checkbox `[checked]` binding | +| `isSelectedInSubmission()` | `this.submission.answer` (@Input, API data) | learner's original submission | `doReview`, `isDisplayOnly` — "Learner's answer" badge | +| `isSelectedInReview()` | `this.review.answer` (@Input, API data) | reviewer's original review | `isDisplayOnly` — "Expert's answer" badge | + +`isSelected()` is used for checkbox bindings in **both** `doAssessment` and `doReview` because it reads from `innerValue` which preserves user edits across pagination. The other two only display static badges from API data. + +--- + +## Testing Verification + +Verified via browser screenshots on `localhost:4200`: + +### Review Mode — "150 Questions" assessment +- Text field: "Persist test!!!" persisted across page 1 → page 4 → page 1 +- Radio (oneof): 2nd choice selection persisted +- Checkbox (multiple): unchecked 1st checkbox stayed unchecked after pagination +- No console errors (previous `TypeError: arrayInput.push` resolved) + +### Assessment Mode — "1 group of 9 questions" assessment +- Multi-team-member-selector: selected `learner_reg_091` and `learner 004`, navigated page 1 → page 2 → page 1, both selections persisted + +### Review Mode — "1 group of 10 questions" assessment +- Radio selections persisted across pagination diff --git a/lambda/README.md b/lambda/README.md index 23c219b34..ede09877e 100644 --- a/lambda/README.md +++ b/lambda/README.md @@ -1,3 +1,10 @@ ### Description -This directory will hold `lambda@edge` functions. \ No newline at end of file +This directory will hold `lambda@edge` functions. + +`forwarder` - lambda function that sits infront of the CDN, handles `globalization` redirection. +`versioner` - function to create lambda function version. + +### Deployment + +Once `AWS` credentials is ready, just run `deploy.sh`. Make sure you installed [sam](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) on your machine. \ No newline at end of file diff --git a/projects/v3/src/app/app.component.ts b/projects/v3/src/app/app.component.ts index bfb27b094..e0ca7dd45 100644 --- a/projects/v3/src/app/app.component.ts +++ b/projects/v3/src/app/app.component.ts @@ -29,7 +29,7 @@ export class AppComponent implements OnInit, OnDestroy { $unsubscribe = new Subject(); lastVisitedUrl: string; - // list of urls that should not be cached + // urls that should not be cached for last visited tracking noneCachedUrl = [ 'devtool', 'registration', @@ -40,6 +40,7 @@ export class AppComponent implements OnInit, OnDestroy { 'direct_login', 'do=secure', 'auth/secure', + 'assessment-mobile/review', 'undefined', ]; diff --git a/projects/v3/src/app/components/assessment/assessment.component.html b/projects/v3/src/app/components/assessment/assessment.component.html index fd4580391..f4a8ceeac 100644 --- a/projects/v3/src/app/components/assessment/assessment.component.html +++ b/projects/v3/src/app/components/assessment/assessment.component.html @@ -1,25 +1,46 @@ -
- +
+
-
+

{{ savingMessage$ | async }}

- + - Reviewer Details + Reviewer Details - - Expert - + + Expert + @@ -29,27 +50,42 @@ + class="ion-no-margin ion-padding ion-padding-horizontal main-content" + role="region" + aria-label="Submission information" + i18n-aria-label> - Submission Details + Submission Details - - Learner - + + Learner + - + - - Team - + + Team + @@ -61,31 +97,42 @@
+
+ [innerHTML]="assessment.name">
- - + + -

+

Due Date:

+ [content]="assessment.description" + id="asmt-des" + class="body-2 black" + [attr.aria-describedby]="randomCode(assessment.name)"> - + -

Locked by

-

Please wait until the user finishes editing

+

Locked by

+

Please wait until the user finishes editing

-
- -
-

- -
+ +
+

+ +
@@ -114,10 +172,13 @@

- - + +  * + *ngIf="shouldShowRequiredIndicator(question)" + aria-label="required" + i18n-aria-label> * - + - -
+
- -
- +
+ + aria-label="Saving answer" + i18n-aria-label + role="img"> + aria-label="Answer saved successfully" + i18n-aria-label + role="img"> - + [attr.aria-label]="'Retry save'" + i18n-aria-label>
- - - No answer for this question. - + + + + No answer for this question. + + -
-

Unsupported question type: {{ question.type }}

+
+

Unsupported question type: {{ question.type }}

+