Skip to content

Commit f860fe8

Browse files
authored
Misc: fixes to CourseInformation, others, in scaffolded courses (#816)
- **add title, logo props** - **markRaw on dynamic component...** - **fixes: proptypes, avoid parent transtion wrapper** - **defensive check agianst malformed tags doc** - **refactor: move getAppliedTags impl inside API**
2 parents 91f7b83 + b11f589 commit f860fe8

File tree

16 files changed

+308
-65
lines changed

16 files changed

+308
-65
lines changed

packages/common-ui/src/components/CourseCardBrowser.vue

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -262,26 +262,19 @@ export default defineComponent({
262262
},
263263
async loadCardTags(cardIds: string[]) {
264264
try {
265-
// Get all tags for the course
266-
const allTags = await this.courseDB!.getCourseTagStubs();
267-
268-
// For each card, find tags that include this card
269-
cardIds.forEach(cardId => {
270-
const cardTags: TagStub[] = [];
271-
272-
// Check each tag to see if it contains this card
273-
allTags.rows.forEach(tagRow => {
274-
if (tagRow.doc && tagRow.doc.taggedCards.includes(cardId)) {
275-
cardTags.push({
276-
name: tagRow.doc.name,
277-
snippet: tagRow.doc.snippet,
278-
count: tagRow.doc.taggedCards.length
279-
});
280-
}
281-
});
282-
283-
this.cardTags[cardId] = cardTags;
284-
});
265+
// Use the proper API method to get tags for each card
266+
await Promise.all(
267+
cardIds.map(async (cardId) => {
268+
const appliedTags = await this.courseDB!.getAppliedTags(cardId);
269+
270+
// Convert to TagStub format
271+
this.cardTags[cardId] = appliedTags.rows.map(row => ({
272+
name: row.value.name,
273+
snippet: row.value.snippet,
274+
count: row.value.count
275+
}));
276+
})
277+
);
285278
} catch (error) {
286279
console.error('Error loading card tags:', error);
287280
}

packages/common-ui/src/components/StudySession.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -595,7 +595,7 @@ export default defineComponent({
595595
596596
this.cardCount++;
597597
this.data = tmpData;
598-
this.view = tmpView;
598+
this.view = markRaw(tmpView);
599599
this.cardID = _cardID;
600600
this.courseID = _courseID;
601601
this.card_elo = tmpCardData.elo.global.score;

packages/common-ui/src/components/auth/UserChip.vue

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ import { getCurrentUser, useAuthStore } from '../../stores/useAuthStore';
9797
import { useConfigStore } from '../../stores/useConfigStore';
9898
import { useAuthUI } from '../../composables/useAuthUI';
9999
100+
// Define props (even if not used, prevents warnings)
101+
defineProps<{
102+
showLoginButton?: boolean;
103+
redirectToPath?: string;
104+
}>();
105+
100106
const router = useRouter();
101107
const authStore = useAuthStore();
102108
const configStore = useConfigStore();

packages/common-ui/src/components/auth/UserLoginAndRegistrationContainer.vue

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
11
<template>
2-
<transition v-if="userReady && display" name="component-fade" mode="out-in">
3-
<div v-if="guestMode && authUIConfig.showLoginRegistration">
4-
<v-dialog v-model="regDialog" width="500px">
5-
<template #activator="{ props }">
6-
<v-btn class="mr-2" size="small" color="success" v-bind="props">Sign Up</v-btn>
7-
</template>
8-
<UserRegistration @toggle="toggle" />
9-
</v-dialog>
10-
<v-dialog v-model="loginDialog" width="500px">
11-
<template #activator="{ props }">
12-
<v-btn size="small" color="success" v-bind="props">Log In</v-btn>
13-
</template>
14-
<UserLogin @toggle="toggle" />
15-
</v-dialog>
16-
</div>
17-
<user-chip v-else />
18-
</transition>
2+
<div v-if="userReady && display">
3+
<transition name="component-fade" mode="out-in">
4+
<div v-if="guestMode && authUIConfig.showLoginRegistration" key="login-buttons">
5+
<v-dialog v-model="regDialog" width="500px">
6+
<template #activator="{ props }">
7+
<v-btn class="mr-2" size="small" color="success" v-bind="props">Sign Up</v-btn>
8+
</template>
9+
<UserRegistration @toggle="toggle" />
10+
</v-dialog>
11+
<v-dialog v-model="loginDialog" width="500px">
12+
<template #activator="{ props }">
13+
<v-btn size="small" color="success" v-bind="props">Log In</v-btn>
14+
</template>
15+
<UserLogin @toggle="toggle" />
16+
</v-dialog>
17+
</div>
18+
<div v-else key="user-chip">
19+
<user-chip />
20+
</div>
21+
</transition>
22+
</div>
1923
</template>
2024

2125
<script lang="ts" setup>
@@ -28,6 +32,12 @@ import { useAuthStore } from '../../stores/useAuthStore';
2832
import { useAuthUI } from '../../composables/useAuthUI';
2933
import { GuestUsername } from '@vue-skuilder/db';
3034
35+
// Define props
36+
const props = defineProps<{
37+
showLoginButton?: boolean;
38+
redirectToPath?: string;
39+
}>();
40+
3141
const route = useRoute();
3242
const authStore = useAuthStore();
3343
const authUI = useAuthUI();

packages/common-ui/src/components/cardRendering/CardLoader.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
</template>
1414

1515
<script lang="ts">
16-
import { defineComponent, PropType } from 'vue';
16+
import { defineComponent, PropType, markRaw } from 'vue';
1717
import { getDataLayer, CardData, CardRecord, DisplayableData } from '@vue-skuilder/db';
1818
import { log, displayableDataToViewData, ViewData, ViewDescriptor } from '@vue-skuilder/common';
1919
import { ViewComponent } from '../../composables';
@@ -92,7 +92,7 @@ export default defineComponent({
9292
}
9393
9494
this.data = tmpData;
95-
this.view = tmpView as ViewComponent;
95+
this.view = markRaw(tmpView as ViewComponent);
9696
this.cardID = _cardID;
9797
this.courseID = _courseID;
9898
} catch (e) {

packages/db/src/impl/common/BaseUserDB.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,14 @@ export class BaseUser implements UserDBInterface, DocumentUpdater {
7979
return !this._username.startsWith(GuestUsername);
8080
}
8181

82-
private remoteDB!: PouchDB.Database;
8382
public remote(): PouchDB.Database {
8483
return this.remoteDB;
8584
}
85+
8686
private localDB!: PouchDB.Database;
87+
private remoteDB!: PouchDB.Database;
88+
private writeDB!: PouchDB.Database; // Database to use for write operations (local-first approach)
89+
8790
private updateQueue!: UpdateQueue;
8891

8992
public async createAccount(
@@ -597,7 +600,11 @@ Currently logged-in as ${this._username}.`
597600
private setDBandQ() {
598601
this.localDB = getLocalUserDB(this._username);
599602
this.remoteDB = this.syncStrategy.setupRemoteDB(this._username);
600-
this.updateQueue = new UpdateQueue(this.localDB);
603+
// writeDB follows local-first pattern: static mode writes to local, CouchDB writes to remote/local as appropriate
604+
this.writeDB = this.syncStrategy.getWriteDB
605+
? this.syncStrategy.getWriteDB(this._username)
606+
: this.localDB;
607+
this.updateQueue = new UpdateQueue(this.localDB, this.writeDB);
601608
}
602609

603610
private async init() {
@@ -697,7 +704,9 @@ Currently logged-in as ${this._username}.`
697704
* @returns The updated state of the card's CardHistory data
698705
*/
699706

700-
public async putCardRecord<T extends CardRecord>(record: T): Promise<CardHistory<CardRecord>> {
707+
public async putCardRecord<T extends CardRecord>(
708+
record: T
709+
): Promise<CardHistory<CardRecord> & PouchDB.Core.RevisionIdMeta> {
701710
const cardHistoryID = getCardHistoryID(record.courseID, record.cardID);
702711
// stringify the current record to make it writable to couchdb
703712
record.timeStamp = moment.utc(record.timeStamp).toString() as unknown as Moment;
@@ -735,8 +744,8 @@ Currently logged-in as ${this._username}.`
735744
streak: 0,
736745
bestInterval: 0,
737746
};
738-
void this.remoteDB.put<CardHistory<T>>(initCardHistory);
739-
return initCardHistory;
747+
const putResult = await this.writeDB.put<CardHistory<T>>(initCardHistory);
748+
return { ...initCardHistory, _rev: putResult.rev };
740749
} else {
741750
throw new Error(`putCardRecord failed because of:
742751
name:${reason.name}
@@ -793,7 +802,7 @@ Currently logged-in as ${this._username}.`
793802
const deletePromises = duplicateDocIds.map(async (docId) => {
794803
try {
795804
const doc = await this.remoteDB.get(docId);
796-
await this.remoteDB.remove(doc);
805+
await this.writeDB.remove(doc);
797806
log(`Successfully removed duplicate review: ${docId}`);
798807
} catch (error) {
799808
log(`Failed to remove duplicate review ${docId}: ${error}`);
@@ -891,7 +900,7 @@ Currently logged-in as ${this._username}.`
891900

892901
if (err.status === 404) {
893902
// doc does not exist. Create it and then run this fcn again.
894-
await this.remoteDB.put<ClassroomRegistrationDoc>({
903+
await this.writeDB.put<ClassroomRegistrationDoc>({
895904
_id: BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS,
896905
registrations: [],
897906
});

packages/db/src/impl/common/SyncStrategy.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ export interface SyncStrategy {
1414
*/
1515
setupRemoteDB(username: string): PouchDB.Database;
1616

17+
/**
18+
* Get the database to use for write operations (local-first approach)
19+
* @param username The username to get write DB for
20+
* @returns PouchDB database instance for write operations
21+
*/
22+
getWriteDB?(username: string): PouchDB.Database;
23+
1724
/**
1825
* Start synchronization between local and remote databases
1926
* @param localDB The local PouchDB instance

packages/db/src/impl/couch/CouchDBSyncStrategy.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,16 @@ export class CouchDBSyncStrategy implements SyncStrategy {
3232
}
3333
}
3434

35+
getWriteDB(username: string): PouchDB.Database {
36+
if (username === GuestUsername || username.startsWith(GuestUsername)) {
37+
// Guest users write to local database
38+
return getLocalUserDB(username);
39+
} else {
40+
// Authenticated users write to remote (which will sync to local)
41+
return this.getUserDB(username);
42+
}
43+
}
44+
3545
startSync(localDB: PouchDB.Database, remoteDB: PouchDB.Database): void {
3646
// Only sync if local and remote are different instances
3747
if (localDB !== remoteDB) {

packages/db/src/impl/couch/updateQueue.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ export default class UpdateQueue extends Loggable {
1212
[index: string]: boolean;
1313
} = {};
1414

15-
private db: PouchDB.Database;
15+
private readDB: PouchDB.Database; // Database for read operations
16+
private writeDB: PouchDB.Database; // Database for write operations (local-first)
1617

1718
public update<T extends PouchDB.Core.Document<object>>(
1819
id: PouchDB.Core.DocumentId,
@@ -27,29 +28,32 @@ export default class UpdateQueue extends Loggable {
2728
return this.applyUpdates<T>(id);
2829
}
2930

30-
constructor(db: PouchDB.Database) {
31+
constructor(readDB: PouchDB.Database, writeDB?: PouchDB.Database) {
3132
super();
3233
// PouchDB.debug.enable('*');
33-
this.db = db;
34+
this.readDB = readDB;
35+
this.writeDB = writeDB || readDB; // Default to readDB if writeDB not provided
3436
logger.debug(`UpdateQ initialized...`);
35-
void this.db.info().then((i) => {
37+
void this.readDB.info().then((i) => {
3638
logger.debug(`db info: ${JSON.stringify(i)}`);
3739
});
3840
}
3941

40-
private async applyUpdates<T extends PouchDB.Core.Document<object>>(id: string): Promise<T> {
42+
private async applyUpdates<T extends PouchDB.Core.Document<object>>(
43+
id: string
44+
): Promise<T & PouchDB.Core.GetMeta & PouchDB.Core.RevisionIdMeta> {
4145
logger.debug(`Applying updates on doc: ${id}`);
4246
if (this.inprogressUpdates[id]) {
4347
// console.log(`Updates in progress...`);
44-
await this.db.info(); // stall for a round trip
48+
await this.readDB.info(); // stall for a round trip
4549
// console.log(`Retrying...`);
4650
return this.applyUpdates<T>(id);
4751
} else {
4852
if (this.pendingUpdates[id] && this.pendingUpdates[id].length > 0) {
4953
this.inprogressUpdates[id] = true;
5054

5155
try {
52-
let doc = await this.db.get<T>(id);
56+
let doc = await this.readDB.get<T>(id);
5357
logger.debug(`Retrieved doc: ${id}`);
5458
while (this.pendingUpdates[id].length !== 0) {
5559
const update = this.pendingUpdates[id].splice(0, 1)[0];
@@ -66,7 +70,7 @@ export default class UpdateQueue extends Loggable {
6670
// console.log(`${k}: ${typeof k}`);
6771
// }
6872
// console.log(`Applied updates to doc: ${JSON.stringify(doc)}`);
69-
await this.db.put<T>(doc);
73+
await this.writeDB.put<T>(doc);
7074
logger.debug(`Put doc: ${id}`);
7175

7276
if (this.pendingUpdates[id].length === 0) {

packages/db/src/impl/static/NoOpSyncStrategy.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ export class NoOpSyncStrategy implements SyncStrategy {
1717
return getLocalUserDB(username);
1818
}
1919

20+
getWriteDB(username: string): PouchDB.Database {
21+
// In static mode, always write to local database
22+
return getLocalUserDB(username);
23+
}
24+
2025
startSync(_localDB: PouchDB.Database, _remoteDB: PouchDB.Database): void {
2126
// No-op - in static mode, local and remote are the same database instance
2227
// PouchDB sync with itself is harmless and efficient

0 commit comments

Comments
 (0)