Skip to content

Commit cb9b948

Browse files
committed
add UI for browsing course content navigators
1 parent f52f1a3 commit cb9b948

File tree

5 files changed

+550
-10
lines changed

5 files changed

+550
-10
lines changed

packages/db/src/impl/pouch/courseDB.ts

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -408,19 +408,47 @@ above:\n${above.rows.map((r) => `\t${r.id}-${r.key}\n`)}`;
408408
////////////////////////////////////
409409

410410
getNavigationStrategy(id: string): Promise<ContentNavigationStrategyData> {
411-
throw new Error(`Method not implemented. getNavigationStrategy(${id})`);
411+
console.log(`[courseDB] Getting navigation strategy: ${id}`);
412+
// For now, just return the ELO strategy regardless of the ID
413+
const strategy: ContentNavigationStrategyData = {
414+
id: 'ELO',
415+
docType: DocType.NAVIGATION_STRATEGY,
416+
name: 'ELO',
417+
description: 'ELO-based navigation strategy for ordering content by difficulty',
418+
implementingClass: Navigators.ELO,
419+
course: this.id,
420+
serializedData: '', // serde is a noop for ELO navigator.
421+
};
422+
return Promise.resolve(strategy);
412423
}
413424

414425
getAllNavigationStrategies(): Promise<ContentNavigationStrategyData[]> {
415-
// [ ] return `default` hard coded ELO-neighbor lookup
416-
throw new Error('Method not implemented.');
426+
console.log('[courseDB] Returning hard-coded navigation strategies');
427+
const strategies: ContentNavigationStrategyData[] = [
428+
{
429+
id: 'ELO',
430+
docType: DocType.NAVIGATION_STRATEGY,
431+
name: 'ELO',
432+
description: 'ELO-based navigation strategy for ordering content by difficulty',
433+
implementingClass: Navigators.ELO,
434+
course: this.id,
435+
serializedData: '', // serde is a noop for ELO navigator.
436+
}
437+
];
438+
return Promise.resolve(strategies);
417439
}
418440

419441
addNavigationStrategy(data: ContentNavigationStrategyData): Promise<void> {
420-
throw new Error(`Method not implemented. addNavigationStrategy(${data})`);
442+
console.log(`[courseDB] Adding navigation strategy: ${data.id}`);
443+
// For now, just log the data and return success
444+
console.log(data);
445+
return Promise.resolve();
421446
}
422447
updateNavigationStrategy(id: string, data: ContentNavigationStrategyData): Promise<void> {
423-
throw new Error(`Method not implemented. updateNavigationStrategy(${id}, ${data})`);
448+
console.log(`[courseDB] Updating navigation strategy: ${id}`);
449+
// For now, just log the data and return success
450+
console.log(data);
451+
return Promise.resolve();
424452
}
425453

426454
async surfaceNavigationStrategy(): Promise<ContentNavigationStrategyData> {

packages/platform-ui/src/components/Edit/CourseEditor.vue

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,12 @@
88
<router-link to="/q">Quilts</router-link> /
99
<router-link :to="`/q/${courseConfig ? courseConfig.name : course}`">{{ courseConfig?.name }}</router-link>
1010
</h1>
11-
<v-btn color="success" @click="toggleComponent">Content Editing / Component Registration</v-btn>
12-
<div v-if="editingMode">
11+
<v-btn-toggle v-model="editorMode" mandatory color="success" class="mb-4">
12+
<v-btn value="content">Content Editing</v-btn>
13+
<v-btn value="component">Component Registration</v-btn>
14+
<v-btn value="navigation">Navigation Strategies</v-btn>
15+
</v-btn-toggle>
16+
<div v-if="editorMode === 'content'">
1317
<v-select
1418
v-model="selectedShape"
1519
label="What kind of content are you adding?"
@@ -22,14 +26,16 @@
2226
:course-cfg="courseConfig"
2327
/>
2428
</div>
25-
<component-registration v-else :course="course" />
29+
<component-registration v-else-if="editorMode === 'component'" :course="course" />
30+
<navigation-strategy-editor v-else-if="editorMode === 'navigation'" :course-id="course" />
2631
</div>
2732
</div>
2833
</template>
2934

3035
<script lang="ts">
3136
import { defineComponent } from 'vue';
3237
import ComponentRegistration from '@/components/Edit/ComponentRegistration/ComponentRegistration.vue';
38+
import NavigationStrategyEditor from '@/components/Edit/NavigationStrategy/NavigationStrategyEditor.vue';
3339
import { allCourses } from '@vue-skuilder/courses';
3440
import { BlanksCard, BlanksCardDataShapes } from '@vue-skuilder/courses';
3541
import { CourseConfig, NameSpacer, DataShape } from '@vue-skuilder/common';
@@ -43,6 +49,7 @@ export default defineComponent({
4349
components: {
4450
DataInputForm,
4551
ComponentRegistration,
52+
NavigationStrategyEditor,
4653
},
4754
4855
props: {
@@ -60,7 +67,7 @@ export default defineComponent({
6067
courseConfig: null as CourseConfig | null,
6168
dataShape: BlanksCardDataShapes[0] as DataShape,
6269
loading: true,
63-
editingMode: true,
70+
editorMode: 'content', // 'content', 'component', or 'navigation'
6471
dataInputFormStore: useDataInputFormStore(),
6572
};
6673
},
@@ -120,7 +127,8 @@ export default defineComponent({
120127
},
121128
122129
toggleComponent() {
123-
this.editingMode = !this.editingMode;
130+
// Legacy method, now handled by v-btn-toggle
131+
this.editorMode = this.editorMode === 'content' ? 'component' : 'content';
124132
},
125133
},
126134
});
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
<template>
2+
<div class="navigation-strategy-editor">
3+
<div v-if="loading">
4+
<v-progress-circular indeterminate color="secondary"></v-progress-circular>
5+
</div>
6+
<div v-else>
7+
<h2 class="text-h5 mb-4">Navigation Strategies</h2>
8+
9+
<div v-if="strategies.length === 0" class="no-strategies">
10+
<p>No navigation strategies defined for this course. Course will be served via default ELO based strategy.</p>
11+
</div>
12+
13+
<navigation-strategy-list
14+
v-else
15+
:strategies="strategies"
16+
:active-strategy-id="activeStrategyId"
17+
@edit="editStrategy"
18+
@activate="activateStrategy"
19+
@delete="confirmDeleteStrategy"
20+
/>
21+
22+
<v-btn color="primary" class="mt-4" @click="createNewStrategy">
23+
<v-icon start>mdi-plus</v-icon>
24+
Add New Strategy
25+
</v-btn>
26+
27+
<v-dialog v-model="showForm" max-width="700px">
28+
<v-card>
29+
<v-card-title class="text-h5">
30+
{{ isEditing ? 'Edit Navigation Strategy' : 'Create Navigation Strategy' }}
31+
</v-card-title>
32+
<v-card-text>
33+
<navigation-strategy-form
34+
:strategy="currentStrategy"
35+
:course-id="courseId"
36+
@save="saveStrategy"
37+
@cancel="cancelEdit"
38+
/>
39+
</v-card-text>
40+
</v-card>
41+
</v-dialog>
42+
43+
<v-dialog v-model="showDeleteConfirm" max-width="400px">
44+
<v-card>
45+
<v-card-title class="text-h5">Delete Strategy</v-card-title>
46+
<v-card-text> Are you sure you want to delete the strategy "{{ strategyToDelete?.name }}"? </v-card-text>
47+
<v-card-actions>
48+
<v-spacer></v-spacer>
49+
<v-btn color="error" @click="deleteStrategy">Delete</v-btn>
50+
<v-btn @click="showDeleteConfirm = false">Cancel</v-btn>
51+
</v-card-actions>
52+
</v-card>
53+
</v-dialog>
54+
</div>
55+
</div>
56+
</template>
57+
58+
<script lang="ts">
59+
import { defineComponent } from 'vue';
60+
import type { ContentNavigationStrategyData } from '@vue-skuilder/db/src/core/types/contentNavigationStrategy';
61+
import NavigationStrategyList from './NavigationStrategyList.vue';
62+
import NavigationStrategyForm from './NavigationStrategyForm.vue';
63+
import { getDataLayer, DocType, Navigators } from '@vue-skuilder/db';
64+
65+
export default defineComponent({
66+
name: 'NavigationStrategyEditor',
67+
68+
components: {
69+
NavigationStrategyList,
70+
NavigationStrategyForm,
71+
},
72+
73+
props: {
74+
courseId: {
75+
type: String,
76+
required: true,
77+
},
78+
},
79+
80+
data() {
81+
return {
82+
strategies: [] as ContentNavigationStrategyData[],
83+
currentStrategy: null as ContentNavigationStrategyData | null,
84+
showForm: false,
85+
isEditing: false,
86+
loading: true,
87+
activeStrategyId: '',
88+
showDeleteConfirm: false,
89+
strategyToDelete: null as ContentNavigationStrategyData | null,
90+
};
91+
},
92+
93+
async created() {
94+
await this.loadStrategies();
95+
},
96+
97+
methods: {
98+
async loadStrategies() {
99+
this.loading = true;
100+
try {
101+
const courseDB = getDataLayer().getCoursesDB(this.courseId);
102+
103+
// Get all navigation strategies
104+
this.strategies = await courseDB.getAllNavigationStrategies();
105+
106+
// Get the active strategy
107+
const activeStrategy = await courseDB.surfaceNavigationStrategy();
108+
this.activeStrategyId = activeStrategy.id;
109+
} catch (error) {
110+
console.error('Failed to load navigation strategies:', error);
111+
// In case of error, use a placeholder
112+
this.strategies = [
113+
{
114+
id: 'ELO',
115+
docType: DocType.NAVIGATION_STRATEGY,
116+
name: 'ELO',
117+
description: 'Default ELO-based navigation strategy',
118+
implementingClass: Navigators.ELO,
119+
course: this.courseId,
120+
serializedData: '',
121+
},
122+
];
123+
this.activeStrategyId = 'ELO';
124+
}
125+
this.loading = false;
126+
},
127+
128+
createNewStrategy() {
129+
this.currentStrategy = {
130+
id: '', // Will be generated when saved
131+
docType: DocType.NAVIGATION_STRATEGY,
132+
name: '',
133+
description: '',
134+
implementingClass: Navigators.ELO, // Default to ELO
135+
course: this.courseId,
136+
serializedData: '',
137+
};
138+
this.isEditing = false;
139+
this.showForm = true;
140+
},
141+
142+
editStrategy(strategy: ContentNavigationStrategyData) {
143+
this.currentStrategy = { ...strategy };
144+
this.isEditing = true;
145+
this.showForm = true;
146+
},
147+
148+
async saveStrategy(strategy: ContentNavigationStrategyData) {
149+
this.loading = true;
150+
try {
151+
const courseDB = getDataLayer().getCoursesDB(this.courseId);
152+
153+
if (this.isEditing) {
154+
// Update existing strategy
155+
await courseDB.updateNavigationStrategy(strategy.id, strategy);
156+
} else {
157+
// For new strategies, generate an ID if not provided
158+
if (!strategy.id) {
159+
strategy.id = `strategy-${Date.now()}`;
160+
}
161+
// Add new strategy
162+
await courseDB.addNavigationStrategy(strategy);
163+
}
164+
165+
// Reload strategies to get the updated list
166+
await this.loadStrategies();
167+
this.showForm = false;
168+
} catch (error) {
169+
console.error('Failed to save navigation strategy:', error);
170+
// In a real app, you would show an error message to the user
171+
}
172+
this.loading = false;
173+
},
174+
175+
cancelEdit() {
176+
this.showForm = false;
177+
this.currentStrategy = null;
178+
},
179+
180+
async activateStrategy(strategyId: string) {
181+
// Set the active strategy ID locally
182+
this.activeStrategyId = strategyId;
183+
184+
// In a real implementation, you would save this preference to the database
185+
// For now, we just log it
186+
console.log(`Strategy ${strategyId} activated`);
187+
188+
// In the future, you might implement:
189+
// await courseDB.setActiveNavigationStrategy(strategyId);
190+
},
191+
192+
confirmDeleteStrategy(strategy: ContentNavigationStrategyData) {
193+
this.strategyToDelete = strategy;
194+
this.showDeleteConfirm = true;
195+
},
196+
197+
async deleteStrategy() {
198+
if (!this.strategyToDelete) return;
199+
200+
this.loading = true;
201+
try {
202+
// In a real implementation, you would call an API to delete the strategy
203+
console.log(`Strategy ${this.strategyToDelete.id} deleted`);
204+
205+
// For now, we only support removal from the local array
206+
// In the future, you would implement:
207+
// await courseDB.deleteNavigationStrategy(this.strategyToDelete.id);
208+
209+
this.strategies = this.strategies.filter((s) => s.id !== this.strategyToDelete?.id);
210+
211+
this.showDeleteConfirm = false;
212+
this.strategyToDelete = null;
213+
} catch (error) {
214+
console.error('Failed to delete navigation strategy:', error);
215+
}
216+
this.loading = false;
217+
},
218+
},
219+
});
220+
</script>
221+
222+
<style scoped>
223+
.navigation-strategy-editor {
224+
padding: 16px;
225+
}
226+
227+
.no-strategies {
228+
margin: 20px 0;
229+
padding: 20px;
230+
background-color: #f5f5f5;
231+
border-radius: 4px;
232+
text-align: center;
233+
}
234+
</style>

0 commit comments

Comments
 (0)