musora-content-services 2.112.2 → 2.113.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/src/services/contentProgress.js +44 -41
- package/src/services/progress-row/rows/method-card.js +2 -1
- package/src/services/sync/models/ContentProgress.ts +5 -4
- package/src/services/sync/repositories/content-progress.ts +17 -5
- package/src/services/sync/schema/index.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
## [2.113.0](https://github.com/railroadmedia/musora-content-services/compare/v2.112.2...v2.113.0) (2026-01-08)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **BEH-1491:** proper card ordering (and hiding) on progress row ([#686](https://github.com/railroadmedia/musora-content-services/issues/686)) ([e519c35](https://github.com/railroadmedia/musora-content-services/commit/e519c352ac5e8d09a910b89fa03baf31490da102))
|
|
11
|
+
|
|
5
12
|
### [2.112.2](https://github.com/railroadmedia/musora-content-services/compare/v2.112.1...v2.112.2) (2026-01-08)
|
|
6
13
|
|
|
7
14
|
|
package/package.json
CHANGED
|
@@ -467,7 +467,7 @@ export async function contentStatusReset(contentId, collection = null, {skipPush
|
|
|
467
467
|
return resetStatus(contentId, collection, {skipPush})
|
|
468
468
|
}
|
|
469
469
|
|
|
470
|
-
async function saveContentProgress(contentId, collection, progress, currentSeconds, {skipPush = false,
|
|
470
|
+
async function saveContentProgress(contentId, collection, progress, currentSeconds, {skipPush = false, fromLearningPath = false} = {}) {
|
|
471
471
|
const isLP = collection?.type === COLLECTION_TYPE.LEARNING_PATH
|
|
472
472
|
|
|
473
473
|
// filter out contentIds that are setting progress lower than existing
|
|
@@ -482,12 +482,14 @@ async function saveContentProgress(contentId, collection, progress, currentSecon
|
|
|
482
482
|
collection,
|
|
483
483
|
progress,
|
|
484
484
|
currentSeconds,
|
|
485
|
-
{skipPush: true,
|
|
485
|
+
{skipPush: true, fromLearningPath}
|
|
486
486
|
)
|
|
487
487
|
// note - previous implementation explicitly did not trickle progress to children here
|
|
488
488
|
// (only to siblings/parents via le bubbles)
|
|
489
489
|
|
|
490
|
+
// skip bubbling if progress hasnt changed
|
|
490
491
|
if (progress === currentProgress) {
|
|
492
|
+
if (!skipPush) db.contentProgress.requestPushUnsynced()
|
|
491
493
|
return
|
|
492
494
|
}
|
|
493
495
|
|
|
@@ -561,45 +563,6 @@ async function setStartedOrCompletedStatus(contentId, collection, isCompleted, {
|
|
|
561
563
|
return response
|
|
562
564
|
}
|
|
563
565
|
|
|
564
|
-
// we cannot simply pass LP id with self collection, because we do not have a-la-carte LP's set up yet,
|
|
565
|
-
// and we need each lesson to bubble to its parent outside of LP
|
|
566
|
-
async function duplicateLearningPathProgressToExternalContents(ids, collection, hierarchy, {skipPush = false} = {}) {
|
|
567
|
-
// filter out LPs. we dont want to duplicate to LP's while we dont have a-la-cart LP's set up.
|
|
568
|
-
let filteredIds = Object.fromEntries(
|
|
569
|
-
Object.entries(ids).filter(([id]) => {
|
|
570
|
-
return hierarchy.parents[parseInt(id)] !== null
|
|
571
|
-
})
|
|
572
|
-
)
|
|
573
|
-
|
|
574
|
-
const extProgresses = await getProgressDataByIds(Object.keys(filteredIds), null)
|
|
575
|
-
|
|
576
|
-
// overwrite if LP progress greater, unless LP progress was reset to 0
|
|
577
|
-
filteredIds = Object.entries(filteredIds).filter(([id, pct]) => {
|
|
578
|
-
const extPct = extProgresses[id]?.progress
|
|
579
|
-
return (pct !== 0)
|
|
580
|
-
? pct > extPct
|
|
581
|
-
: false
|
|
582
|
-
})
|
|
583
|
-
|
|
584
|
-
// each handles its own bubbling.
|
|
585
|
-
// skipPush on all but last to avoid multiple push requests
|
|
586
|
-
filteredIds.forEach(([id, pct], index) => {
|
|
587
|
-
let skip = true
|
|
588
|
-
if (index === filteredIds.length - 1) {
|
|
589
|
-
skip = skipPush
|
|
590
|
-
}
|
|
591
|
-
saveContentProgress(parseInt(id), null, pct, null, {skipPush: skip, hideFromProgressRow: true})
|
|
592
|
-
})
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
async function getHierarchy(contentId, collection) {
|
|
596
|
-
if (collection && collection.type === COLLECTION_TYPE.LEARNING_PATH) {
|
|
597
|
-
return await fetchLearningPathHierarchy(contentId, collection)
|
|
598
|
-
} else {
|
|
599
|
-
return await fetchHierarchy(contentId)
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
|
|
603
566
|
async function setStartedOrCompletedStatusMany(contentIds, collection, isCompleted, {skipPush = false} = {}) {
|
|
604
567
|
const isLP = collection?.type === COLLECTION_TYPE.LEARNING_PATH
|
|
605
568
|
const progress = isCompleted ? 100 : 0
|
|
@@ -651,6 +614,7 @@ async function resetStatus(contentId, collection = null, {skipPush = false} = {}
|
|
|
651
614
|
|
|
652
615
|
const progress = 0
|
|
653
616
|
const response = await db.contentProgress.eraseProgress(contentId, collection, {skipPush: true})
|
|
617
|
+
|
|
654
618
|
const hierarchy = await getHierarchy(contentId, collection)
|
|
655
619
|
|
|
656
620
|
let progresses = {
|
|
@@ -670,6 +634,45 @@ async function resetStatus(contentId, collection = null, {skipPush = false} = {}
|
|
|
670
634
|
return response
|
|
671
635
|
}
|
|
672
636
|
|
|
637
|
+
// we cannot simply pass LP id with self collection, because we do not have a-la-carte LP's set up yet,
|
|
638
|
+
// and we need each lesson to bubble to its parent outside of LP
|
|
639
|
+
async function duplicateLearningPathProgressToExternalContents(ids, collection, hierarchy, {skipPush = false} = {}) {
|
|
640
|
+
// filter out LPs. we dont want to duplicate to LP's while we dont have a-la-cart LP's set up.
|
|
641
|
+
let filteredIds = Object.fromEntries(
|
|
642
|
+
Object.entries(ids).filter(([id]) => {
|
|
643
|
+
return hierarchy.parents[parseInt(id)] !== null
|
|
644
|
+
})
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
const extProgresses = await getProgressDataByIds(Object.keys(filteredIds), null)
|
|
648
|
+
|
|
649
|
+
// overwrite if LP progress greater, unless LP progress was reset to 0
|
|
650
|
+
filteredIds = Object.entries(filteredIds).filter(([id, pct]) => {
|
|
651
|
+
const extPct = extProgresses[id]?.progress
|
|
652
|
+
return (pct !== 0)
|
|
653
|
+
? pct > extPct
|
|
654
|
+
: false
|
|
655
|
+
})
|
|
656
|
+
|
|
657
|
+
// each handles its own bubbling.
|
|
658
|
+
// skipPush on all but last to avoid multiple push requests
|
|
659
|
+
filteredIds.forEach(([id, pct], index) => {
|
|
660
|
+
let skip = true
|
|
661
|
+
if (index === filteredIds.length - 1) {
|
|
662
|
+
skip = skipPush
|
|
663
|
+
}
|
|
664
|
+
saveContentProgress(parseInt(id), null, pct, null, {skipPush: skip, fromLearningPath: true})
|
|
665
|
+
})
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
async function getHierarchy(contentId, collection) {
|
|
669
|
+
if (collection && collection.type === COLLECTION_TYPE.LEARNING_PATH) {
|
|
670
|
+
return await fetchLearningPathHierarchy(contentId, collection)
|
|
671
|
+
} else {
|
|
672
|
+
return await fetchHierarchy(contentId)
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
673
676
|
// agnostic to collection - makes returned data structure simpler,
|
|
674
677
|
// as long as callers remember to pass collection where needed
|
|
675
678
|
function trickleProgress(hierarchy, contentId, _collection, progress) {
|
|
@@ -107,7 +107,8 @@ export async function getMethodCard(brand) {
|
|
|
107
107
|
)
|
|
108
108
|
|
|
109
109
|
if (!maxProgressTimestamp) {
|
|
110
|
-
|
|
110
|
+
// active LP created_at is stored in seconds, so *1000 to match rest of cards
|
|
111
|
+
maxProgressTimestamp = learningPath.active_learning_path_created_at * 1000
|
|
111
112
|
}
|
|
112
113
|
|
|
113
114
|
return {
|
|
@@ -29,6 +29,7 @@ export default class ContentProgress extends BaseModel<{
|
|
|
29
29
|
state: STATE
|
|
30
30
|
progress_percent: number
|
|
31
31
|
resume_time_seconds: number | null
|
|
32
|
+
last_interacted_a_la_carte: number | null
|
|
32
33
|
}> {
|
|
33
34
|
static table = SYNC_TABLES.CONTENT_PROGRESS
|
|
34
35
|
|
|
@@ -53,8 +54,8 @@ export default class ContentProgress extends BaseModel<{
|
|
|
53
54
|
get resume_time_seconds() {
|
|
54
55
|
return (this._getRaw('resume_time_seconds') as number) || null
|
|
55
56
|
}
|
|
56
|
-
get
|
|
57
|
-
return this._getRaw('
|
|
57
|
+
get last_interacted_a_la_carte() {
|
|
58
|
+
return this._getRaw('last_interacted_a_la_carte') as number
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
set content_id(value: number) {
|
|
@@ -90,8 +91,8 @@ export default class ContentProgress extends BaseModel<{
|
|
|
90
91
|
throwIfNotNullableNumber(value)
|
|
91
92
|
this._setRaw('resume_time_seconds', value !== null ? throwIfOutsideRange(value, 0, 65535) : value)
|
|
92
93
|
}
|
|
93
|
-
set
|
|
94
|
-
this._setRaw('
|
|
94
|
+
set last_interacted_a_la_carte(value: number) {
|
|
95
|
+
this._setRaw('last_interacted_a_la_carte', value)
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
}
|
|
@@ -62,7 +62,7 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
|
|
|
62
62
|
Q.where('collection_type', COLLECTION_TYPE.SELF),
|
|
63
63
|
Q.where('collection_id', COLLECTION_ID_SELF),
|
|
64
64
|
|
|
65
|
-
Q.where('
|
|
65
|
+
Q.where('last_interacted_a_la_carte', Q.notEq(null)),
|
|
66
66
|
|
|
67
67
|
Q.or(Q.where('state', STATE.STARTED), Q.where('state', STATE.COMPLETED)),
|
|
68
68
|
Q.sortBy('updated_at', 'desc'),
|
|
@@ -135,9 +135,13 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
|
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
recordProgress(contentId: number, collection: CollectionParameter | null, progressPct: number, resumeTime?: number, {skipPush = false,
|
|
138
|
+
recordProgress(contentId: number, collection: CollectionParameter | null, progressPct: number, resumeTime?: number, {skipPush = false, fromLearningPath = false} = {}) {
|
|
139
139
|
const id = ProgressRepository.generateId(contentId, collection)
|
|
140
140
|
|
|
141
|
+
if (collection?.type === COLLECTION_TYPE.LEARNING_PATH) {
|
|
142
|
+
fromLearningPath = true
|
|
143
|
+
}
|
|
144
|
+
|
|
141
145
|
const result = this.upsertOne(id, (r) => {
|
|
142
146
|
r.content_id = contentId
|
|
143
147
|
r.collection_type = collection?.type ?? COLLECTION_TYPE.SELF
|
|
@@ -149,7 +153,10 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
|
|
|
149
153
|
r.resume_time_seconds = Math.floor(resumeTime)
|
|
150
154
|
}
|
|
151
155
|
|
|
152
|
-
|
|
156
|
+
if (!fromLearningPath) {
|
|
157
|
+
r.last_interacted_a_la_carte = Date.now()
|
|
158
|
+
}
|
|
159
|
+
|
|
153
160
|
}, { skipPush })
|
|
154
161
|
|
|
155
162
|
// Emit event AFTER database write completes
|
|
@@ -180,8 +187,11 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
|
|
|
180
187
|
recordProgressMany(
|
|
181
188
|
contentProgresses: Record<string, number>, // Accept plain object
|
|
182
189
|
collection: CollectionParameter | null,
|
|
183
|
-
{ tentative = true, skipPush = false,
|
|
190
|
+
{ tentative = true, skipPush = false, fromLearningPath = false }: { tentative?: boolean; skipPush?: boolean; fromLearningPath?: boolean } = {}
|
|
184
191
|
) {
|
|
192
|
+
if (collection?.type === COLLECTION_TYPE.LEARNING_PATH) {
|
|
193
|
+
fromLearningPath = true
|
|
194
|
+
}
|
|
185
195
|
|
|
186
196
|
const data = Object.fromEntries(
|
|
187
197
|
Object.entries(contentProgresses).map(([contentId, progressPct]) => [
|
|
@@ -193,7 +203,9 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
|
|
|
193
203
|
|
|
194
204
|
r.progress_percent = progressPct
|
|
195
205
|
|
|
196
|
-
|
|
206
|
+
if (!fromLearningPath) {
|
|
207
|
+
r.last_interacted_a_la_carte = Date.now()
|
|
208
|
+
}
|
|
197
209
|
},
|
|
198
210
|
])
|
|
199
211
|
)
|
|
@@ -26,7 +26,7 @@ const contentProgressTable = tableSchema({
|
|
|
26
26
|
{ name: 'state', type: 'string', isIndexed: true },
|
|
27
27
|
{ name: 'progress_percent', type: 'number' },
|
|
28
28
|
{ name: 'resume_time_seconds', type: 'number', isOptional: true },
|
|
29
|
-
{ name: '
|
|
29
|
+
{ name: 'last_interacted_a_la_carte', type: 'number', isOptional: true },
|
|
30
30
|
{ name: 'created_at', type: 'number' },
|
|
31
31
|
{ name: 'updated_at', type: 'number', isIndexed: true }
|
|
32
32
|
]
|