From ed1ddd0850bd6d8f855e15b4ff4e7f191722ac82 Mon Sep 17 00:00:00 2001
From: sunil <sunil@practera.com>
Date: Sat, 15 Jun 2024 09:30:43 +0530
Subject: [PATCH 1/4] deploy

---
 .github/workflows/p2-sandbox-appv3.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/p2-sandbox-appv3.yml b/.github/workflows/p2-sandbox-appv3.yml
index 9611af378..83c7ceb7a 100644
--- a/.github/workflows/p2-sandbox-appv3.yml
+++ b/.github/workflows/p2-sandbox-appv3.yml
@@ -16,7 +16,7 @@ on:
         default: ''
   push:
     branches:
-      - 'develop'
+      - 'pvt'
 
 
 ################################################

From 68291ed4cbb8348575537a47c1d3f99e7c9b8a47 Mon Sep 17 00:00:00 2001
From: trtshen <cshen_581@hotmail.com>
Date: Sun, 30 Jun 2024 02:38:22 +0800
Subject: [PATCH 2/4] [CORE-6667] auto-refresh upon newItem pusher event

---
 .../components/activity/activity.component.ts | 46 +++++++++++++------
 .../activity-desktop/activity-desktop.page.ts | 16 ++++++-
 2 files changed, 46 insertions(+), 16 deletions(-)

diff --git a/projects/v3/src/app/components/activity/activity.component.ts b/projects/v3/src/app/components/activity/activity.component.ts
index 484dbbccb..97282cea4 100644
--- a/projects/v3/src/app/components/activity/activity.component.ts
+++ b/projects/v3/src/app/components/activity/activity.component.ts
@@ -1,4 +1,5 @@
-import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
+import { Subject } from 'rxjs';
+import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core';
 import { SharedService } from '@v3/app/services/shared.service';
 import { UnlockIndicatorService } from '@v3/app/services/unlock-indicator.service';
 import { Activity, ActivityService, Task } from '@v3/services/activity.service';
@@ -6,13 +7,14 @@ import { Submission } from '@v3/services/assessment.service';
 import { NotificationsService } from '@v3/services/notifications.service';
 import { BrowserStorageService } from '@v3/services/storage.service';
 import { UtilsService } from '@v3/services/utils.service';
+import { takeUntil } from 'rxjs/operators';
 
 @Component({
   selector: 'app-activity',
   templateUrl: './activity.component.html',
   styleUrls: ['./activity.component.scss'],
 })
-export class ActivityComponent implements OnInit, OnChanges {
+export class ActivityComponent implements OnInit, OnChanges, OnDestroy {
   @Input() activity: Activity;
   @Input() currentTask: Task;
   @Input() submission: Submission;
@@ -25,6 +27,7 @@ export class ActivityComponent implements OnInit, OnChanges {
   // false: at least one non-team task
   @Output() cannotAccessTeamActivity = new EventEmitter();
   isForTeamOnly: boolean = false;
+  private unsubscribe$: Subject<any> = new Subject();
 
   constructor(
     private utils: UtilsService,
@@ -32,17 +35,26 @@ export class ActivityComponent implements OnInit, OnChanges {
     private notificationsService: NotificationsService,
     private sharedService: SharedService,
     private activityService: ActivityService,
-    private unlockIndicatorService: UnlockIndicatorService,
+    private unlockIndicatorService: UnlockIndicatorService
   ) {}
 
+  ngOnDestroy(): void {
+    this.unsubscribe$.next();
+    this.unsubscribe$.complete();
+  }
+
+  resetTaskIndicator(unlockedTasks) {
+    this.newTasks = {};
+    unlockedTasks.forEach((task) => {
+      this.newTasks[task.taskId] = true;
+    });
+  }
+
   ngOnInit() {
     this.leadImage = this.storageService.getUser().programImage;
-    this.unlockIndicatorService.unlockedTasks$.subscribe((unlockedTasks) => {
-      this.newTasks = {};
-      unlockedTasks.forEach((task) => {
-        this.newTasks[task.taskId] = true;
-      });
-    });
+    this.unlockIndicatorService.unlockedTasks$
+      .pipe(takeUntil(this.unsubscribe$))
+      .subscribe(this.resetTaskIndicator);
   }
 
   ngOnChanges(changes: SimpleChanges): void {
@@ -56,23 +68,27 @@ export class ActivityComponent implements OnInit, OnChanges {
 
       const currentValue = changes.activity.currentValue;
       if (currentValue.tasks?.length > 0) {
-        this.activityService.nonTeamActivity(changes.activity.currentValue?.tasks).then((nonTeamActivity) => {
-          this.isForTeamOnly = !nonTeamActivity;
-          this.cannotAccessTeamActivity.emit(this.isForTeamOnly);
-        });
+        this.activityService
+          .nonTeamActivity(changes.activity.currentValue?.tasks)
+          .then((nonTeamActivity) => {
+            this.isForTeamOnly = !nonTeamActivity;
+            this.cannotAccessTeamActivity.emit(this.isForTeamOnly);
+          });
 
         const unlockedTasks = this.unlockIndicatorService.getTasksByActivity(this.activity);
         if (unlockedTasks.length === 0) {
           const clearedActivities = this.unlockIndicatorService.clearActivity(this.activity.id);
           clearedActivities.forEach((activity) => {
-            this.notificationsService.markTodoItemAsDone(activity).subscribe();
+            this.notificationsService
+              .markTodoItemAsDone(activity)
+              .pipe(takeUntil(this.unsubscribe$))
+              .subscribe();
           });
         }
       }
     }
   }
 
-
   /**
    * Task icon type
    *
diff --git a/projects/v3/src/app/pages/activity-desktop/activity-desktop.page.ts b/projects/v3/src/app/pages/activity-desktop/activity-desktop.page.ts
index 7ea3220e8..ab1d0d5b8 100644
--- a/projects/v3/src/app/pages/activity-desktop/activity-desktop.page.ts
+++ b/projects/v3/src/app/pages/activity-desktop/activity-desktop.page.ts
@@ -1,3 +1,4 @@
+import { UnlockIndicatorService } from './../../services/unlock-indicator.service';
 import { DOCUMENT } from '@angular/common';
 import { Component, Inject, OnInit } from '@angular/core';
 import { ActivatedRoute, Router } from '@angular/router';
@@ -45,7 +46,8 @@ export class ActivityDesktopPage {
     private notificationsService: NotificationsService,
     private storageService: BrowserStorageService,
     private utils: UtilsService,
-    @Inject(DOCUMENT) private readonly document: Document
+    private unlockIndicatorService: UnlockIndicatorService,
+    @Inject(DOCUMENT) private readonly document: Document,
   ) { }
 
   ionViewWillEnter() {
@@ -117,6 +119,7 @@ export class ActivityDesktopPage {
       });
     }));
 
+    // refresh when review is available (AI review, peer review, etc.)
     this.subscriptions.push(
       this.utils.getEvent('notification').subscribe(event => {
         const review = event?.meta?.AssessmentReview;
@@ -127,6 +130,17 @@ export class ActivityDesktopPage {
         }
       })
     );
+
+    // check new unlock indicator to refresh
+    this.subscriptions.push(
+      this.unlockIndicatorService.unlockedTasks$.subscribe(unlockedTasks => {
+        if (this.activity) {
+          if (unlockedTasks.some(task => task.activityId === this.activity.id)) {
+            this.activityService.getActivity(this.activity.id);
+          }
+        }
+      })
+    );
   }
 
   ionViewWillLeave() {

From b6ab84dcba7849ab3875cc0975124d7b453cc1ae Mon Sep 17 00:00:00 2001
From: trtshen <cshen_581@hotmail.com>
Date: Sun, 30 Jun 2024 03:10:51 +0800
Subject: [PATCH 3/4] [CORE-6667] show instant indicator for unlock

---
 .../v3/src/app/components/activity/activity.component.ts | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/projects/v3/src/app/components/activity/activity.component.ts b/projects/v3/src/app/components/activity/activity.component.ts
index 97282cea4..7e61561ff 100644
--- a/projects/v3/src/app/components/activity/activity.component.ts
+++ b/projects/v3/src/app/components/activity/activity.component.ts
@@ -45,9 +45,11 @@ export class ActivityComponent implements OnInit, OnChanges, OnDestroy {
 
   resetTaskIndicator(unlockedTasks) {
     this.newTasks = {};
-    unlockedTasks.forEach((task) => {
-      this.newTasks[task.taskId] = true;
-    });
+    unlockedTasks
+      .filter((task) => task.taskId)
+      .forEach((task) => {
+        this.newTasks[task.taskId] = true;
+      });
   }
 
   ngOnInit() {
@@ -76,6 +78,7 @@ export class ActivityComponent implements OnInit, OnChanges, OnDestroy {
           });
 
         const unlockedTasks = this.unlockIndicatorService.getTasksByActivity(this.activity);
+        this.resetTaskIndicator(unlockedTasks);
         if (unlockedTasks.length === 0) {
           const clearedActivities = this.unlockIndicatorService.clearActivity(this.activity.id);
           clearedActivities.forEach((activity) => {

From 6d5adb791ff15ace2cc38e0491332d3853e70f89 Mon Sep 17 00:00:00 2001
From: trtshen <cshen_581@hotmail.com>
Date: Tue, 2 Jul 2024 14:33:20 +0800
Subject: [PATCH 4/4] [CORE-6673] revalidate on visit

---
 projects/v3/src/app/pages/home/home.page.ts   | 118 +++++++++++-------
 .../app/services/unlock-indicator.service.ts  |  19 +--
 2 files changed, 73 insertions(+), 64 deletions(-)

diff --git a/projects/v3/src/app/pages/home/home.page.ts b/projects/v3/src/app/pages/home/home.page.ts
index 7f8f90496..bd1b2ecd1 100644
--- a/projects/v3/src/app/pages/home/home.page.ts
+++ b/projects/v3/src/app/pages/home/home.page.ts
@@ -1,6 +1,9 @@
 import { Component, OnInit, OnDestroy } from '@angular/core';
 import { NavigationEnd, Router } from '@angular/router';
-import { Achievement, AchievementService } from '@v3/app/services/achievement.service';
+import {
+  Achievement,
+  AchievementService,
+} from '@v3/app/services/achievement.service';
 import { ActivityService } from '@v3/app/services/activity.service';
 import { AssessmentService } from '@v3/app/services/assessment.service';
 import { NotificationsService } from '@v3/app/services/notifications.service';
@@ -34,7 +37,7 @@ export class HomePage implements OnInit, OnDestroy {
   getIsPointsConfigured: boolean = false;
   getEarnedPoints: number = 0;
   hasUnlockedTasks: Object = {};
-  unlockedMilestones: {[key: number]: boolean} = {};
+  unlockedMilestones: { [key: number]: boolean } = {};
 
   // default card image (gracefully show broken url)
   defaultLeadImage: string = '';
@@ -57,57 +60,67 @@ export class HomePage implements OnInit, OnDestroy {
   ngOnInit() {
     this.isMobile = this.utils.isMobile();
     this.subscriptions = [];
-    this.subscriptions.push(this.homeService.milestones$.pipe(
-      distinctUntilChanged(),
-      filter(milestones => milestones !== null),
-    ).subscribe(
-      res => {
-        this.milestones = res;
-      }
-    ));
-    this.subscriptions.push(this.achievementService.achievements$.subscribe(
-      res => {
+    this.subscriptions.push(
+      this.homeService.milestones$
+        .pipe(
+          distinctUntilChanged(),
+          filter((milestones) => milestones !== null)
+        )
+        .subscribe((res) => {
+          this.milestones = res;
+        })
+    );
+    this.subscriptions.push(
+      this.achievementService.achievements$.subscribe((res) => {
         this.achievements = res;
-      }
-    ));
-    this.subscriptions.push(this.homeService.experienceProgress$.subscribe(
-      res => {
+      })
+    );
+    this.subscriptions.push(
+      this.homeService.experienceProgress$.subscribe((res) => {
         this.experienceProgress = res;
-      }
-    ));
-    this.subscriptions.push(this.homeService.projectProgress$.pipe(
-      filter(progress => progress !== null),
-    ).subscribe(
-      progress => {
-        progress?.milestones?.forEach(m => {
-          m.activities?.forEach(a => this.activityProgresses[a.id] = a.progress);
-        });
-      }
-    ));
+      })
+    );
+    this.subscriptions.push(
+      this.homeService.projectProgress$
+        .pipe(filter((progress) => progress !== null))
+        .subscribe((progress) => {
+          progress?.milestones?.forEach((m) => {
+            m.activities?.forEach(
+              (a) => (this.activityProgresses[a.id] = a.progress)
+            );
+          });
+        })
+    );
 
     this.subscriptions.push(
-      this.router.events.subscribe(event => {
+      this.router.events.subscribe((event) => {
         if (event instanceof NavigationEnd) {
           this.updateDashboard();
         }
       })
     );
 
-    this.unlockIndicatorService.unlockedTasks$.subscribe(unlockedTasks => {
+    this.unlockIndicatorService.unlockedTasks$.subscribe((unlockedTasks) => {
       this.hasUnlockedTasks = {}; // reset
       this.unlockedMilestones = {}; // reset
-      unlockedTasks.forEach(task => {
+      unlockedTasks.forEach((task) => {
         if (task.milestoneId) {
-          this.unlockedMilestones[task.milestoneId] = true;
+          if (this.unlockIndicatorService.isMilestoneClearable(task.milestoneId)) {
+            this.verifyUnlockedMilestoneValidity(task.milestoneId);
+          } else {
+            this.unlockedMilestones[task.milestoneId] = true;
+          }
         }
 
-        this.hasUnlockedTasks[task.activityId] = true;
+        if (task.activityId) {
+          this.hasUnlockedTasks[task.activityId] = true;
+        }
       });
     });
   }
 
   ngOnDestroy(): void {
-    this.subscriptions.forEach(s => s.unsubscribe());
+    this.subscriptions.forEach((s) => s.unsubscribe());
   }
 
   async updateDashboard() {
@@ -170,32 +183,27 @@ export class HomePage implements OnInit, OnDestroy {
    * @param keyboardEvent The keyboard event object, if the function was called by a keyboard event.
    * @returns A Promise that resolves when the navigation is complete.
    */
-  async gotoActivity({activity, milestone}, keyboardEvent?: KeyboardEvent) {
-    if (keyboardEvent && (keyboardEvent?.code === 'Space' || keyboardEvent?.code === 'Enter')) {
+  async gotoActivity({ activity, milestone }, keyboardEvent?: KeyboardEvent) {
+    if (
+      keyboardEvent &&
+      (keyboardEvent?.code === 'Space' || keyboardEvent?.code === 'Enter')
+    ) {
       keyboardEvent.preventDefault();
     } else if (keyboardEvent) {
       return;
     }
 
     if (activity.isLocked) {
-      return ;
+      return;
     }
 
     this.activityService.clearActivity();
     this.assessmentService.clearAssessment();
 
-    // check & update unlocked milestones
     if (this.unlockIndicatorService.isMilestoneClearable(milestone.id)) {
-      const unlockedMilestones = this.unlockIndicatorService.clearActivity(milestone.id);
-      unlockedMilestones.forEach(unlockedMilestone => {
-        this.notification.markTodoItemAsDone(unlockedMilestone).pipe(first()).subscribe(() => {
-          // eslint-disable-next-line no-console
-          console.log('Marked milestone as done', unlockedMilestone);
-        });
-      });
+      this.verifyUnlockedMilestoneValidity(milestone.id);
     }
 
-
     if (!this.isMobile) {
       return this.router.navigate(['v3', 'activity-desktop', activity.id]);
     }
@@ -203,8 +211,26 @@ export class HomePage implements OnInit, OnDestroy {
     return this.router.navigate(['v3', 'activity-mobile', activity.id]);
   }
 
+  verifyUnlockedMilestoneValidity(milestoneId: number): void {
+    // check & update unlocked milestones
+    const unlockedMilestones =
+      this.unlockIndicatorService.clearActivity(milestoneId);
+    unlockedMilestones.forEach((unlockedMilestone) => {
+      this.notification
+        .markTodoItemAsDone(unlockedMilestone)
+        .pipe(first())
+        .subscribe(() => {
+          // eslint-disable-next-line no-console
+          console.log('Marked milestone as done', unlockedMilestone);
+        });
+    });
+  }
+
   achievePopup(achievement: Achievement, keyboardEvent?: KeyboardEvent): void {
-    if (keyboardEvent && (keyboardEvent?.code === 'Space' || keyboardEvent?.code === 'Enter')) {
+    if (
+      keyboardEvent &&
+      (keyboardEvent?.code === 'Space' || keyboardEvent?.code === 'Enter')
+    ) {
       keyboardEvent.preventDefault();
     } else if (keyboardEvent) {
       return;
diff --git a/projects/v3/src/app/services/unlock-indicator.service.ts b/projects/v3/src/app/services/unlock-indicator.service.ts
index b6a113f8e..8ee3ebcb3 100644
--- a/projects/v3/src/app/services/unlock-indicator.service.ts
+++ b/projects/v3/src/app/services/unlock-indicator.service.ts
@@ -74,7 +74,7 @@ export class UnlockIndicatorService {
   /**
    * Clear all tasks related to a particular activity
    *
-   * @param   {number[]}        id  [id description]
+   * @param   {number[]}        id  can either be activityId or milestoneId
    *
    * @return  {UnlockedTask[]}      unlocked tasks that were cleared
    */
@@ -130,23 +130,6 @@ export class UnlockIndicatorService {
     return removedTask;
   }
 
-  dedupStored(records) {
-    const uniqueRecords = new Map();
-
-    records.forEach(record => {
-      // Determine the type of identifier and create a unique key
-      const key = `milestoneId:${record.milestoneId || 'none'}-activityId:${record.activityId || 'none'}-taskId:${record.taskId || 'none'}`;
-
-      // If the key doesn't exist in the map, add the record
-      if (!uniqueRecords.has(key)) {
-        uniqueRecords.set(key, record);
-      }
-    });
-
-    // Return an array of unique records
-    return Array.from(uniqueRecords.values());
-  }
-
   // Method to transform and deduplicate the data
   transformAndDeduplicateTodoItem(data) {
     const uniqueEntries = new Map();
