Skip to content

Commit bcdee58

Browse files
committed
[CORE-6673] make sure orphaned indicators get cleared
1 parent fe21046 commit bcdee58

File tree

8 files changed

+876
-38
lines changed

8 files changed

+876
-38
lines changed

docs/unlock-indicator.md

Lines changed: 392 additions & 0 deletions
Large diffs are not rendered by default.

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,8 @@ export class ActivityComponent implements OnInit, OnChanges, OnDestroy {
116116
const unlockedTasks = this.unlockIndicatorService.getTasksByActivity(this.activity);
117117
this.resetTaskIndicator(unlockedTasks);
118118
if (unlockedTasks.length === 0) {
119-
const clearedActivities = this.unlockIndicatorService.clearActivity(this.activity.id);
119+
// handle inaccurate unlock indicators
120+
const clearedActivities = this.unlockIndicatorService.clearRelatedIndicators('activity', this.activity.id);
120121
clearedActivities.forEach((activity) => {
121122
this.notificationsService
122123
.markTodoItemAsDone(activity)

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Component, Input, isDevMode, SimpleChanges } from '@angular/core';
1+
import { Component, Input, isDevMode, OnChanges, SimpleChanges } from '@angular/core';
22
import { getData, getAllTags } from 'exif-js';
33

44
const getImageClassToFixOrientation = (orientation) => {
@@ -32,7 +32,7 @@ const swapWidthAndHeight = img => {
3232
templateUrl: './img.component.html',
3333
styleUrls: ['./img.component.scss']
3434
})
35-
export class ImgComponent {
35+
export class ImgComponent implements OnChanges {
3636
@Input() alt: string;
3737
@Input() imgSrc: string;
3838
proxiedImgSrc: string;

projects/v3/src/app/pages/activity-desktop/activity-desktop.page.ts

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -302,10 +302,81 @@ export class ActivityDesktopPage {
302302
}
303303
});
304304
}
305+
// Clear pure activity-level unlock indicators on page enter/update
306+
this._clearPureActivityIndicator(res.id);
305307
return;
306308
}
307309

308310
this.activity = res;
311+
// Clear pure activity-level unlock indicators on initial set
312+
this._clearPureActivityIndicator(res.id);
313+
}
314+
315+
/**
316+
* clears activity-level unlock indicators on page enter
317+
*/
318+
private _clearPureActivityIndicator(activityId: number): void {
319+
if (!activityId) { return; }
320+
321+
try {
322+
// First try the enhanced approach that handles duplicates
323+
const currentTodoItems = this.notificationsService.getCurrentTodoItems();
324+
const entries = this.unlockIndicatorService.getTasksByActivityId(activityId);
325+
326+
if (entries?.length > 0 && entries.every(e => e.taskId === undefined)) {
327+
// handles server-side duplicates and hierarchy
328+
const result = this.unlockIndicatorService.clearByActivityIdWithDuplicates(activityId, currentTodoItems);
329+
330+
// Mark all duplicate TodoItems as done (bulk operation)
331+
if (result.duplicatesToMark.length > 0) {
332+
const markingOps = this.notificationsService.markMultipleTodoItemsAsDone(result.duplicatesToMark);
333+
markingOps.forEach(op => op.subscribe({
334+
// eslint-disable-next-line no-console
335+
next: (response) => console.log('Marked duplicate activity TodoItem as done:', response),
336+
// eslint-disable-next-line no-console
337+
error: (error) => console.error('Failed to mark activity TodoItem as done:', error)
338+
}));
339+
}
340+
341+
// handles cascade milestone clearing
342+
result.cascadeMilestones.forEach(milestoneData => {
343+
if (milestoneData.duplicatesToMark.length > 0) {
344+
// eslint-disable-next-line no-console
345+
console.log(`Cascade clearing milestone ${milestoneData.milestoneId} with ${milestoneData.duplicatesToMark.length} duplicates`);
346+
const milestoneMarkingOps = this.notificationsService.markMultipleTodoItemsAsDone(milestoneData.duplicatesToMark);
347+
milestoneMarkingOps.forEach(op => op.subscribe({
348+
// eslint-disable-next-line no-console
349+
next: (response) => console.log('Marked cascade milestone TodoItem as done:', response),
350+
// eslint-disable-next-line no-console
351+
error: (error) => console.error('Failed to mark cascade milestone TodoItem as done:', error)
352+
}));
353+
}
354+
});
355+
356+
// Fallback: mark cleared localStorage items as done (for backward compatibility)
357+
result.clearedUnlocks?.forEach(todo => {
358+
this.notificationsService.markTodoItemAsDone(todo).subscribe();
359+
});
360+
return;
361+
}
362+
363+
// If standard approach didn't find anything, try robust clearing for inaccurate data
364+
const relatedIndicators = this.unlockIndicatorService.findRelatedIndicators('activity', activityId);
365+
if (relatedIndicators?.length > 0) {
366+
// Only clear if they are pure activity-level (no task-specific entries)
367+
const pureActivityIndicators = relatedIndicators.filter(r => r.taskId === undefined);
368+
if (pureActivityIndicators.length > 0) {
369+
const cleared = this.unlockIndicatorService.clearRelatedIndicators('activity', activityId);
370+
cleared?.forEach(todo => {
371+
this.notificationsService.markTodoItemAsDone(todo).subscribe();
372+
});
373+
}
374+
}
375+
} catch (e) {
376+
// swallow to avoid breaking page enter; optional logging can be added under dev flag
377+
// eslint-disable-next-line no-console
378+
console.debug('[unlock-indicator] cleanup skipped for activity', activityId, e);
379+
}
309380
}
310381

311382
/**
@@ -396,25 +467,25 @@ export class ActivityDesktopPage {
396467
try {
397468
// handle unexpected submission: do final status check before saving
398469
let hasSubmssion = false;
399-
const { submission } = await this.assessmentService
400-
.fetchAssessment(
470+
const { submission } = await firstValueFrom(
471+
this.assessmentService.fetchAssessment(
401472
event.assessmentId,
402473
'assessment',
403474
this.activity.id,
404475
event.contextId,
405476
event.submissionId
406477
)
407-
.toPromise();
478+
);
408479

409480
if (submission?.status === 'in progress') {
410-
const saved = await this.assessmentService
411-
.submitAssessment(
481+
const saved = await firstValueFrom(
482+
this.assessmentService.submitAssessment(
412483
event.submissionId,
413484
event.assessmentId,
414485
event.contextId,
415486
event.answers
416487
)
417-
.toPromise();
488+
);
418489

419490
// http 200 but error
420491
if (

projects/v3/src/app/pages/activity-mobile/activity-mobile.page.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { ActivatedRoute, Router } from '@angular/router';
33
import { ActivityService, Task, Activity } from '@v3/services/activity.service';
44
import { AssessmentService, Submission } from '@v3/services/assessment.service';
55
import { filter } from 'rxjs/operators';
6+
import { UnlockIndicatorService } from '@v3/app/services/unlock-indicator.service';
7+
import { NotificationsService } from '@v3/app/services/notifications.service';
68

79
@Component({
810
selector: 'app-activity-mobile',
@@ -18,18 +20,58 @@ export class ActivityMobilePage implements OnInit {
1820
private router: Router,
1921
private activityService: ActivityService,
2022
private assessmentService: AssessmentService,
23+
private unlockIndicatorService: UnlockIndicatorService,
24+
private notificationsService: NotificationsService,
2125
) { }
2226

2327
ngOnInit() {
2428
this.activityService.activity$
2529
.pipe(filter(res => res?.id === +this.route.snapshot.paramMap.get('id')))
26-
.subscribe(res => this.activity = res);
30+
.subscribe(res => {
31+
this.activity = res;
32+
if (res?.id) {
33+
this.clearPureActivityIndicator(res.id);
34+
}
35+
});
2736
this.assessmentService.submission$.subscribe(res => this.submission = res);
2837
this.route.params.subscribe(params => {
2938
this.activityService.getActivity(+params.id, false);
3039
});
3140
}
3241

42+
/**
43+
* Clear activity-level-only unlock indicators when entering the activity page.
44+
* Uses robust clearing to handle inaccurate unlock indicator data.
45+
*/
46+
private clearPureActivityIndicator(activityId: number) {
47+
if (!activityId) { return; }
48+
49+
try {
50+
// First try the standard approach
51+
const entries = this.unlockIndicatorService.getTasksByActivityId(activityId);
52+
if (entries?.length > 0 && entries.every(e => e.taskId === undefined)) {
53+
const cleared = this.unlockIndicatorService.clearByActivityId(activityId);
54+
cleared?.forEach(todo => this.notificationsService.markTodoItemAsDone(todo).subscribe());
55+
return;
56+
}
57+
58+
// If standard approach didn't find anything, try robust clearing for inaccurate data
59+
const relatedIndicators = this.unlockIndicatorService.findRelatedIndicators('activity', activityId);
60+
if (relatedIndicators?.length > 0) {
61+
// Only clear if they are pure activity-level (no task-specific entries)
62+
const pureActivityIndicators = relatedIndicators.filter(r => r.taskId === undefined);
63+
if (pureActivityIndicators.length > 0) {
64+
const cleared = this.unlockIndicatorService.clearRelatedIndicators('activity', activityId);
65+
cleared?.forEach(todo => this.notificationsService.markTodoItemAsDone(todo).subscribe());
66+
}
67+
}
68+
} catch (e) {
69+
// swallow to avoid breaking page enter; optional logging can be added under dev flag
70+
// eslint-disable-next-line no-console
71+
console.debug('[unlock-indicator] cleanup skipped for activity', activityId, e);
72+
}
73+
}
74+
3375
goToTask(task: Task) {
3476
this.activityService.goToTask(task, false);
3577
switch (task.type) {

projects/v3/src/app/pages/home/home.page.ts

Lines changed: 69 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -287,18 +287,49 @@ export class HomePage implements OnInit, OnDestroy, AfterViewChecked {
287287
}
288288

289289
if (this.unlockIndicatorService.isActivityClearable(activity.id)) {
290-
const clearedActivityTodo = this.unlockIndicatorService.clearActivity(
291-
activity.id
292-
);
293-
clearedActivityTodo?.forEach((todo) => {
294-
this.notification
295-
.markTodoItemAsDone(todo)
296-
.pipe(first())
297-
.subscribe(() => {
290+
// handles server-side duplicates and hierarchy
291+
const currentTodoItems = this.notification.getCurrentTodoItems();
292+
const result = this.unlockIndicatorService.clearByActivityIdWithDuplicates(activity.id, currentTodoItems);
293+
294+
// Mark all duplicate TodoItems as done (bulk operation)
295+
if (result.duplicatesToMark.length > 0) {
296+
const markingOps = this.notification.markMultipleTodoItemsAsDone(result.duplicatesToMark);
297+
markingOps.forEach(op => op.pipe(first()).subscribe({
298+
// eslint-disable-next-line no-console
299+
next: (response) => console.log('Marked duplicate activity TodoItem as done:', response),
300+
// eslint-disable-next-line no-console
301+
error: (error) => console.error('Failed to mark activity TodoItem as done:', error)
302+
}));
303+
}
304+
305+
// Handle cascade milestone clearing
306+
result.cascadeMilestones.forEach(milestoneData => {
307+
if (milestoneData.duplicatesToMark.length > 0) {
308+
// eslint-disable-next-line no-console
309+
console.log(`Cascade clearing milestone ${milestoneData.milestoneId} with ${milestoneData.duplicatesToMark.length} duplicates`);
310+
const milestoneMarkingOps = this.notification.markMultipleTodoItemsAsDone(milestoneData.duplicatesToMark);
311+
milestoneMarkingOps.forEach(op => op.pipe(first()).subscribe({
298312
// eslint-disable-next-line no-console
299-
console.log("Marked activity as done", todo);
300-
});
313+
next: (response) => console.log('Marked cascade milestone TodoItem as done:', response),
314+
// eslint-disable-next-line no-console
315+
error: (error) => console.error('Failed to mark cascade milestone TodoItem as done:', error)
316+
}));
317+
}
301318
});
319+
320+
// Fallback: if no duplicates found, try robust clearing for inaccurate data
321+
if (result.duplicatesToMark.length === 0) {
322+
const fallbackCleared = this.unlockIndicatorService.clearRelatedIndicators('activity', activity.id);
323+
fallbackCleared?.forEach((todo) => {
324+
this.notification
325+
.markTodoItemAsDone(todo)
326+
.pipe(first())
327+
.subscribe(() => {
328+
// eslint-disable-next-line no-console
329+
console.log("Marked activity as done (fallback)", todo);
330+
});
331+
});
332+
}
302333
}
303334

304335
if (this.unlockIndicatorService.isMilestoneClearable(milestone.id)) {
@@ -318,18 +349,34 @@ export class HomePage implements OnInit, OnDestroy, AfterViewChecked {
318349
* @return {void}
319350
*/
320351
verifyUnlockedMilestoneValidity(milestoneId: number): void {
321-
// check & update unlocked milestones
322-
const unlockedMilestones =
323-
this.unlockIndicatorService.clearActivity(milestoneId);
324-
unlockedMilestones.forEach((unlockedMilestone) => {
325-
this.notification
326-
.markTodoItemAsDone(unlockedMilestone)
327-
.pipe(first())
328-
.subscribe(() => {
329-
// eslint-disable-next-line no-console
330-
console.log("Marked milestone as done", unlockedMilestone);
331-
});
332-
});
352+
// Use enhanced clearing that handles server-side duplicates
353+
const currentTodoItems = this.notification.getCurrentTodoItems();
354+
const result = this.unlockIndicatorService.clearByMilestoneIdWithDuplicates(milestoneId, currentTodoItems);
355+
356+
// Mark all duplicate TodoItems as done (bulk operation)
357+
if (result.duplicatesToMark.length > 0) {
358+
const markingOps = this.notification.markMultipleTodoItemsAsDone(result.duplicatesToMark);
359+
markingOps.forEach(op => op.pipe(first()).subscribe({
360+
// eslint-disable-next-line no-console
361+
next: (response) => console.log('Marked duplicate milestone TodoItem as done:', response),
362+
// eslint-disable-next-line no-console
363+
error: (error) => console.error('Failed to mark milestone TodoItem as done:', error)
364+
}));
365+
}
366+
367+
// Fallback: if no duplicates found, try robust clearing for inaccurate data
368+
if (result.duplicatesToMark.length === 0) {
369+
const fallbackCleared = this.unlockIndicatorService.clearRelatedIndicators('milestone', milestoneId);
370+
fallbackCleared.forEach((unlockedMilestone) => {
371+
this.notification
372+
.markTodoItemAsDone(unlockedMilestone)
373+
.pipe(first())
374+
.subscribe(() => {
375+
// eslint-disable-next-line no-console
376+
console.log("Marked milestone as done (fallback)", unlockedMilestone);
377+
});
378+
});
379+
}
333380
}
334381

335382
async onTrackInfo() {

projects/v3/src/app/services/notifications.service.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,8 @@ export class NotificationsService {
471471
return this.modal(FastFeedbackComponent, props, modalConfig);
472472
}
473473

474+
private currentTodoItems: {id: number, identifier: string}[] = [];
475+
474476
getTodoItems(): Observable<any> {
475477
return this.request
476478
.get(api.get.todoItem, {
@@ -481,6 +483,15 @@ export class NotificationsService {
481483
.pipe(
482484
map((response) => {
483485
if (response.success && response.data) {
486+
// Store current TodoItems for duplicate detection
487+
this.currentTodoItems = response.data.map(item => ({
488+
id: item.id,
489+
identifier: item.identifier
490+
}));
491+
492+
// Clean up orphaned unlock indicators before normalizing
493+
this.unlockIndicatorService.cleanupOrphanedIndicators(response.data);
494+
484495
const normalised = this._normaliseTodoItems(response.data);
485496
this.notifications = normalised;
486497
this._notification$.next(this.notifications);
@@ -490,6 +501,13 @@ export class NotificationsService {
490501
);
491502
}
492503

504+
/**
505+
* Get current TodoItems for duplicate detection
506+
*/
507+
getCurrentTodoItems(): {id: number, identifier: string}[] {
508+
return this.currentTodoItems;
509+
}
510+
493511
/**
494512
* group TodoItems into different types
495513
* - AssessmentReview
@@ -1045,6 +1063,25 @@ export class NotificationsService {
10451063
});
10461064
}
10471065

1066+
/**
1067+
* Mark multiple todo items as done (bulk operation)
1068+
* Handles server-side duplicates for same unlock indicator
1069+
*/
1070+
markMultipleTodoItemsAsDone(items: { identifier?: string; id?: number }[]) {
1071+
const markingOperations = items.map(item =>
1072+
this.markTodoItemAsDone(item).pipe(
1073+
// Add error handling for individual items
1074+
map(response => ({ success: true, item, response })),
1075+
// Don't let individual failures stop the whole bulk operation
1076+
// catchError(error => of({ success: false, item, error }))
1077+
)
1078+
);
1079+
1080+
// eslint-disable-next-line no-console
1081+
console.log(`Bulk marking ${items.length} TodoItems as done:`, items);
1082+
return markingOperations;
1083+
}
1084+
10481085
async trackInfo() {
10491086
const modal = await this.modalController.create({
10501087
component: PopUpComponent,

0 commit comments

Comments
 (0)