Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 13 additions & 20 deletions packages/common-ui/src/components/CourseCardBrowser.vue
Original file line number Diff line number Diff line change
Expand Up @@ -262,26 +262,19 @@ export default defineComponent({
},
async loadCardTags(cardIds: string[]) {
try {
// Get all tags for the course
const allTags = await this.courseDB!.getCourseTagStubs();

// For each card, find tags that include this card
cardIds.forEach(cardId => {
const cardTags: TagStub[] = [];

// Check each tag to see if it contains this card
allTags.rows.forEach(tagRow => {
if (tagRow.doc && tagRow.doc.taggedCards.includes(cardId)) {
cardTags.push({
name: tagRow.doc.name,
snippet: tagRow.doc.snippet,
count: tagRow.doc.taggedCards.length
});
}
});

this.cardTags[cardId] = cardTags;
});
// Use the proper API method to get tags for each card
await Promise.all(
cardIds.map(async (cardId) => {
const appliedTags = await this.courseDB!.getAppliedTags(cardId);

// Convert to TagStub format
this.cardTags[cardId] = appliedTags.rows.map(row => ({
name: row.value.name,
snippet: row.value.snippet,
count: row.value.count
}));
})
);
} catch (error) {
console.error('Error loading card tags:', error);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/common-ui/src/components/StudySession.vue
Original file line number Diff line number Diff line change
Expand Up @@ -595,7 +595,7 @@ export default defineComponent({

this.cardCount++;
this.data = tmpData;
this.view = tmpView;
this.view = markRaw(tmpView);
this.cardID = _cardID;
this.courseID = _courseID;
this.card_elo = tmpCardData.elo.global.score;
Expand Down
6 changes: 6 additions & 0 deletions packages/common-ui/src/components/auth/UserChip.vue
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ import { getCurrentUser, useAuthStore } from '../../stores/useAuthStore';
import { useConfigStore } from '../../stores/useConfigStore';
import { useAuthUI } from '../../composables/useAuthUI';

// Define props (even if not used, prevents warnings)
defineProps<{
showLoginButton?: boolean;
redirectToPath?: string;
}>();

const router = useRouter();
const authStore = useAuthStore();
const configStore = useConfigStore();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
<template>
<transition v-if="userReady && display" name="component-fade" mode="out-in">
<div v-if="guestMode && authUIConfig.showLoginRegistration">
<v-dialog v-model="regDialog" width="500px">
<template #activator="{ props }">
<v-btn class="mr-2" size="small" color="success" v-bind="props">Sign Up</v-btn>
</template>
<UserRegistration @toggle="toggle" />
</v-dialog>
<v-dialog v-model="loginDialog" width="500px">
<template #activator="{ props }">
<v-btn size="small" color="success" v-bind="props">Log In</v-btn>
</template>
<UserLogin @toggle="toggle" />
</v-dialog>
</div>
<user-chip v-else />
</transition>
<div v-if="userReady && display">
<transition name="component-fade" mode="out-in">
<div v-if="guestMode && authUIConfig.showLoginRegistration" key="login-buttons">
<v-dialog v-model="regDialog" width="500px">
<template #activator="{ props }">
<v-btn class="mr-2" size="small" color="success" v-bind="props">Sign Up</v-btn>
</template>
<UserRegistration @toggle="toggle" />
</v-dialog>
<v-dialog v-model="loginDialog" width="500px">
<template #activator="{ props }">
<v-btn size="small" color="success" v-bind="props">Log In</v-btn>
</template>
<UserLogin @toggle="toggle" />
</v-dialog>
</div>
<div v-else key="user-chip">
<user-chip />
</div>
</transition>
</div>
</template>

<script lang="ts" setup>
Expand All @@ -28,6 +32,12 @@ import { useAuthStore } from '../../stores/useAuthStore';
import { useAuthUI } from '../../composables/useAuthUI';
import { GuestUsername } from '@vue-skuilder/db';

// Define props
const props = defineProps<{
showLoginButton?: boolean;
redirectToPath?: string;
}>();

const route = useRoute();
const authStore = useAuthStore();
const authUI = useAuthUI();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
</template>

<script lang="ts">
import { defineComponent, PropType } from 'vue';
import { defineComponent, PropType, markRaw } from 'vue';
import { getDataLayer, CardData, CardRecord, DisplayableData } from '@vue-skuilder/db';
import { log, displayableDataToViewData, ViewData, ViewDescriptor } from '@vue-skuilder/common';
import { ViewComponent } from '../../composables';
Expand Down Expand Up @@ -92,7 +92,7 @@ export default defineComponent({
}

this.data = tmpData;
this.view = tmpView as ViewComponent;
this.view = markRaw(tmpView as ViewComponent);
this.cardID = _cardID;
this.courseID = _courseID;
} catch (e) {
Expand Down
23 changes: 16 additions & 7 deletions packages/db/src/impl/common/BaseUserDB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,14 @@ export class BaseUser implements UserDBInterface, DocumentUpdater {
return !this._username.startsWith(GuestUsername);
}

private remoteDB!: PouchDB.Database;
public remote(): PouchDB.Database {
return this.remoteDB;
}

private localDB!: PouchDB.Database;
private remoteDB!: PouchDB.Database;
private writeDB!: PouchDB.Database; // Database to use for write operations (local-first approach)

private updateQueue!: UpdateQueue;

public async createAccount(
Expand Down Expand Up @@ -597,7 +600,11 @@ Currently logged-in as ${this._username}.`
private setDBandQ() {
this.localDB = getLocalUserDB(this._username);
this.remoteDB = this.syncStrategy.setupRemoteDB(this._username);
this.updateQueue = new UpdateQueue(this.localDB);
// writeDB follows local-first pattern: static mode writes to local, CouchDB writes to remote/local as appropriate
this.writeDB = this.syncStrategy.getWriteDB
? this.syncStrategy.getWriteDB(this._username)
: this.localDB;
this.updateQueue = new UpdateQueue(this.localDB, this.writeDB);
}

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

public async putCardRecord<T extends CardRecord>(record: T): Promise<CardHistory<CardRecord>> {
public async putCardRecord<T extends CardRecord>(
record: T
): Promise<CardHistory<CardRecord> & PouchDB.Core.RevisionIdMeta> {
const cardHistoryID = getCardHistoryID(record.courseID, record.cardID);
// stringify the current record to make it writable to couchdb
record.timeStamp = moment.utc(record.timeStamp).toString() as unknown as Moment;
Expand Down Expand Up @@ -735,8 +744,8 @@ Currently logged-in as ${this._username}.`
streak: 0,
bestInterval: 0,
};
void this.remoteDB.put<CardHistory<T>>(initCardHistory);
return initCardHistory;
const putResult = await this.writeDB.put<CardHistory<T>>(initCardHistory);
return { ...initCardHistory, _rev: putResult.rev };
} else {
throw new Error(`putCardRecord failed because of:
name:${reason.name}
Expand Down Expand Up @@ -793,7 +802,7 @@ Currently logged-in as ${this._username}.`
const deletePromises = duplicateDocIds.map(async (docId) => {
try {
const doc = await this.remoteDB.get(docId);
await this.remoteDB.remove(doc);
await this.writeDB.remove(doc);
log(`Successfully removed duplicate review: ${docId}`);
} catch (error) {
log(`Failed to remove duplicate review ${docId}: ${error}`);
Expand Down Expand Up @@ -891,7 +900,7 @@ Currently logged-in as ${this._username}.`

if (err.status === 404) {
// doc does not exist. Create it and then run this fcn again.
await this.remoteDB.put<ClassroomRegistrationDoc>({
await this.writeDB.put<ClassroomRegistrationDoc>({
_id: BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS,
registrations: [],
});
Expand Down
7 changes: 7 additions & 0 deletions packages/db/src/impl/common/SyncStrategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ export interface SyncStrategy {
*/
setupRemoteDB(username: string): PouchDB.Database;

/**
* Get the database to use for write operations (local-first approach)
* @param username The username to get write DB for
* @returns PouchDB database instance for write operations
*/
getWriteDB?(username: string): PouchDB.Database;

/**
* Start synchronization between local and remote databases
* @param localDB The local PouchDB instance
Expand Down
10 changes: 10 additions & 0 deletions packages/db/src/impl/couch/CouchDBSyncStrategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ export class CouchDBSyncStrategy implements SyncStrategy {
}
}

getWriteDB(username: string): PouchDB.Database {
if (username === GuestUsername || username.startsWith(GuestUsername)) {
// Guest users write to local database
return getLocalUserDB(username);
} else {
// Authenticated users write to remote (which will sync to local)
return this.getUserDB(username);
}
}

startSync(localDB: PouchDB.Database, remoteDB: PouchDB.Database): void {
// Only sync if local and remote are different instances
if (localDB !== remoteDB) {
Expand Down
20 changes: 12 additions & 8 deletions packages/db/src/impl/couch/updateQueue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ export default class UpdateQueue extends Loggable {
[index: string]: boolean;
} = {};

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

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

constructor(db: PouchDB.Database) {
constructor(readDB: PouchDB.Database, writeDB?: PouchDB.Database) {
super();
// PouchDB.debug.enable('*');
this.db = db;
this.readDB = readDB;
this.writeDB = writeDB || readDB; // Default to readDB if writeDB not provided
logger.debug(`UpdateQ initialized...`);
void this.db.info().then((i) => {
void this.readDB.info().then((i) => {
logger.debug(`db info: ${JSON.stringify(i)}`);
});
}

private async applyUpdates<T extends PouchDB.Core.Document<object>>(id: string): Promise<T> {
private async applyUpdates<T extends PouchDB.Core.Document<object>>(
id: string
): Promise<T & PouchDB.Core.GetMeta & PouchDB.Core.RevisionIdMeta> {
logger.debug(`Applying updates on doc: ${id}`);
if (this.inprogressUpdates[id]) {
// console.log(`Updates in progress...`);
await this.db.info(); // stall for a round trip
await this.readDB.info(); // stall for a round trip
// console.log(`Retrying...`);
return this.applyUpdates<T>(id);
} else {
if (this.pendingUpdates[id] && this.pendingUpdates[id].length > 0) {
this.inprogressUpdates[id] = true;

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

if (this.pendingUpdates[id].length === 0) {
Expand Down
5 changes: 5 additions & 0 deletions packages/db/src/impl/static/NoOpSyncStrategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ export class NoOpSyncStrategy implements SyncStrategy {
return getLocalUserDB(username);
}

getWriteDB(username: string): PouchDB.Database {
// In static mode, always write to local database
return getLocalUserDB(username);
}

startSync(_localDB: PouchDB.Database, _remoteDB: PouchDB.Database): void {
// No-op - in static mode, local and remote are the same database instance
// PouchDB sync with itself is harmless and efficient
Expand Down
57 changes: 49 additions & 8 deletions packages/db/src/impl/static/courseDB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,14 +164,55 @@ export class StaticCourseDB implements CourseDBInterface {
}));
}

async getAppliedTags(_cardId: string): Promise<PouchDB.Query.Response<TagStub>> {
// Would need to query the tag index
logger.warn(`getAppliedTags not implemented`);
return {
total_rows: 0,
offset: 0,
rows: [],
};
async getAppliedTags(cardId: string): Promise<PouchDB.Query.Response<TagStub>> {
try {
const tagsIndex = await this.unpacker.getTagsIndex();
const cardTags = tagsIndex.byCard[cardId] || [];

const rows = await Promise.all(
cardTags.map(async (tagName) => {
const tagId = `${DocType.TAG}-${tagName}`;

try {
// Try to get the full tag document
const tagDoc = await this.unpacker.getDocument(tagId);
return {
id: tagId,
key: cardId,
value: {
name: tagDoc.name,
snippet: tagDoc.snippet,
count: tagDoc.taggedCards?.length || 0,
},
};
} catch (error) {
// If tag document not found, create a minimal stub
return {
id: tagId,
key: cardId,
value: {
name: tagName,
snippet: `Tag: ${tagName}`,
count: tagsIndex.byTag[tagName]?.length || 0,
},
};
}
})
);

return {
total_rows: rows.length,
offset: 0,
rows,
};
} catch (error) {
logger.error(`Error getting applied tags for card ${cardId}:`, error);
return {
total_rows: 0,
offset: 0,
rows: [],
};
}
}

async addTagToCard(_cardId: string, _tagId: string): Promise<PouchDB.Core.Response> {
Expand Down
8 changes: 7 additions & 1 deletion packages/standalone-ui/src/components/CourseHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ import { ref, computed } from 'vue';
import { useDisplay } from 'vuetify';
import { UserLoginAndRegistrationContainer } from '@vue-skuilder/common-ui';

// Define props
defineProps<{
title?: string;
logo?: string;
}>();

const { mobile } = useDisplay();
const isMobile = computed(() => mobile.value);
const drawer = ref(false);
Expand All @@ -44,6 +50,6 @@ const menuItems = ref([
{ text: 'Home', path: '/' },
{ text: 'Study', path: '/study' },
{ text: 'Browse', path: '/browse' },
{ text: 'Progress', path: '/progress' },
// Progress view not implemented - will be accessible via UserChip->Stats
]);
</script>
Loading
Loading