@vue-skuilder/cli 0.1.22 → 0.1.23

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.
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.1.22",
6
+ "version": "0.1.23",
7
7
  "type": "module",
8
8
  "main": "./dist-lib/questions.cjs.js",
9
9
  "module": "./dist-lib/questions.mjs",
@@ -58,5 +58,5 @@
58
58
  "vue-tsc": "^1.8.0",
59
59
  "wait-on": "8.0.2"
60
60
  },
61
- "stableVersion": "0.1.22"
61
+ "stableVersion": "0.1.23"
62
62
  }
@@ -5,6 +5,7 @@ import ProgressView from '../views/ProgressView.vue';
5
5
  import BrowseView from '../views/BrowseView.vue';
6
6
  import UserStatsView from '../views/UserStatsView.vue';
7
7
  import UserSettingsView from '../views/UserSettingsView.vue';
8
+ import AdminRegisterQuestionsView from '../views/AdminRegisterQuestionsView.vue';
8
9
  import { UserLogin, UserRegistration } from '@vue-skuilder/common-ui';
9
10
 
10
11
  const routes: Array<RouteRecordRaw> = [
@@ -55,6 +56,14 @@ const routes: Array<RouteRecordRaw> = [
55
56
  component: () => import('../views/TagViewer.vue'),
56
57
  props: true,
57
58
  },
59
+ {
60
+ path: '/admin/register-questions',
61
+ name: 'AdminRegisterQuestions',
62
+ component: AdminRegisterQuestionsView,
63
+ meta: {
64
+ requiresAuth: true,
65
+ },
66
+ },
58
67
  ];
59
68
 
60
69
  const router = createRouter({
@@ -62,4 +71,25 @@ const router = createRouter({
62
71
  routes,
63
72
  });
64
73
 
74
+ // Navigation guard for protected routes
75
+ router.beforeEach(async (to, _from, next) => {
76
+ // Check if route requires authentication
77
+ if (to.meta.requiresAuth) {
78
+ // Dynamically import auth store to avoid circular dependencies
79
+ const { useAuthStore } = await import('@vue-skuilder/common-ui');
80
+ const authStore = useAuthStore();
81
+
82
+ if (!authStore.currentUser) {
83
+ // Redirect to login with return path
84
+ next({
85
+ name: 'login',
86
+ query: { redirect: to.fullPath },
87
+ });
88
+ return;
89
+ }
90
+ }
91
+
92
+ next();
93
+ });
94
+
65
95
  export default router;
@@ -0,0 +1,306 @@
1
+ <template>
2
+ <v-container>
3
+ <v-card>
4
+ <v-card-title class="text-h4 mb-4">
5
+ Register Question Types
6
+ </v-card-title>
7
+
8
+ <!-- Static Mode Warning -->
9
+ <v-alert v-if="isStaticMode" type="warning" variant="tonal" class="ma-4">
10
+ <v-alert-title>Static Mode Detected</v-alert-title>
11
+ <p class="mb-2">
12
+ Question type registration is not available in static mode.
13
+ </p>
14
+ <p class="mb-2">
15
+ To register custom question types, use Studio mode:
16
+ </p>
17
+ <v-code tag="pre" class="text-caption">yarn studio</v-code>
18
+ <p class="mt-2">
19
+ Studio mode provides a development environment with full database access for registering question types.
20
+ </p>
21
+ </v-alert>
22
+
23
+ <!-- Loading State -->
24
+ <v-card-text v-else-if="loading">
25
+ <v-progress-circular indeterminate color="primary" />
26
+ <span class="ml-4">Loading question type data...</span>
27
+ </v-card-text>
28
+
29
+ <!-- Error State -->
30
+ <v-alert v-else-if="error" type="error" variant="tonal" class="ma-4">
31
+ <v-alert-title>Error</v-alert-title>
32
+ {{ error }}
33
+ </v-alert>
34
+
35
+ <!-- Main Content -->
36
+ <v-card-text v-else>
37
+ <!-- Current State -->
38
+ <div class="mb-6">
39
+ <h3 class="text-h6 mb-2">Current CourseConfig</h3>
40
+ <v-list density="compact">
41
+ <v-list-item>
42
+ <template #prepend>
43
+ <v-icon>mdi-shape</v-icon>
44
+ </template>
45
+ <v-list-item-title>
46
+ {{ currentDataShapeCount }} DataShapes registered
47
+ </v-list-item-title>
48
+ </v-list-item>
49
+ <v-list-item>
50
+ <template #prepend>
51
+ <v-icon>mdi-help-circle</v-icon>
52
+ </template>
53
+ <v-list-item-title>
54
+ {{ currentQuestionTypeCount }} QuestionTypes registered
55
+ </v-list-item-title>
56
+ </v-list-item>
57
+ </v-list>
58
+ </div>
59
+
60
+ <!-- Custom Questions -->
61
+ <div class="mb-6">
62
+ <h3 class="text-h6 mb-2">From allCustomQuestions()</h3>
63
+ <v-list density="compact">
64
+ <v-list-item>
65
+ <template #prepend>
66
+ <v-icon>mdi-shape</v-icon>
67
+ </template>
68
+ <v-list-item-title>
69
+ {{ processedDataShapes.length }} DataShapes found
70
+ </v-list-item-title>
71
+ </v-list-item>
72
+ <v-list-item>
73
+ <template #prepend>
74
+ <v-icon>mdi-help-circle</v-icon>
75
+ </template>
76
+ <v-list-item-title>
77
+ {{ processedQuestions.length }} QuestionTypes found
78
+ </v-list-item-title>
79
+ </v-list-item>
80
+ </v-list>
81
+ </div>
82
+
83
+ <!-- Changes Preview -->
84
+ <div v-if="hasChanges" class="mb-6">
85
+ <h3 class="text-h6 mb-2">Changes to Apply</h3>
86
+ <v-list density="compact">
87
+ <v-list-item
88
+ v-for="change in changesList"
89
+ :key="change.key"
90
+ :class="change.type === 'add' ? 'text-success' : 'text-info'"
91
+ >
92
+ <template #prepend>
93
+ <v-icon :color="change.type === 'add' ? 'success' : 'info'">
94
+ {{ change.type === 'add' ? 'mdi-plus' : 'mdi-update' }}
95
+ </v-icon>
96
+ </template>
97
+ <v-list-item-title>
98
+ {{ change.label }}
99
+ </v-list-item-title>
100
+ </v-list-item>
101
+ </v-list>
102
+ </div>
103
+
104
+ <!-- No Changes Message -->
105
+ <v-alert v-else type="info" variant="tonal">
106
+ All question types are already registered. No changes needed.
107
+ </v-alert>
108
+
109
+ <!-- Success Message -->
110
+ <v-alert v-if="successMessage" type="success" variant="tonal" class="mb-4">
111
+ <v-alert-title>Success</v-alert-title>
112
+ {{ successMessage }}
113
+ </v-alert>
114
+ </v-card-text>
115
+
116
+ <!-- Actions -->
117
+ <v-card-actions v-if="!isStaticMode && !loading && !error">
118
+ <v-spacer />
119
+ <v-btn
120
+ color="primary"
121
+ :disabled="!hasChanges || registering"
122
+ :loading="registering"
123
+ @click="registerQuestions"
124
+ >
125
+ Register Question Types
126
+ </v-btn>
127
+ </v-card-actions>
128
+ </v-card>
129
+ </v-container>
130
+ </template>
131
+
132
+ <script setup lang="ts">
133
+ import { ref, computed, onMounted } from 'vue';
134
+ import { getDataLayer } from '@vue-skuilder/db';
135
+ import {
136
+ registerCustomQuestionTypes,
137
+ processCustomQuestionsData,
138
+ isDataShapeRegistered,
139
+ isQuestionTypeRegistered,
140
+ isDataShapeSchemaAvailable,
141
+ type CustomQuestionsData,
142
+ type ProcessedDataShape,
143
+ type ProcessedQuestionData,
144
+ } from '@vue-skuilder/db';
145
+ import { useAuthStore } from '@vue-skuilder/common-ui';
146
+ import { allCustomQuestions } from '../questions';
147
+ import config from '../../skuilder.config.json';
148
+
149
+ // State
150
+ const loading = ref(true);
151
+ const error = ref<string | null>(null);
152
+ const registering = ref(false);
153
+ const successMessage = ref<string | null>(null);
154
+
155
+ const courseConfig = ref<any>(null);
156
+ const customQuestions = ref<CustomQuestionsData | null>(null);
157
+ const processedDataShapes = ref<ProcessedDataShape[]>([]);
158
+ const processedQuestions = ref<ProcessedQuestionData[]>([]);
159
+
160
+ // Check if in static mode
161
+ const isStaticMode = computed(() => config.dataLayerType === 'static');
162
+
163
+ // Computed properties
164
+ const currentDataShapeCount = computed(() => courseConfig.value?.dataShapes?.length ?? 0);
165
+ const currentQuestionTypeCount = computed(() => courseConfig.value?.questionTypes?.length ?? 0);
166
+
167
+ interface Change {
168
+ key: string;
169
+ type: 'add' | 'update';
170
+ label: string;
171
+ }
172
+
173
+ const changesList = computed<Change[]>(() => {
174
+ if (!courseConfig.value || !customQuestions.value) return [];
175
+
176
+ const changes: Change[] = [];
177
+
178
+ // Check DataShapes
179
+ for (const dataShape of processedDataShapes.value) {
180
+ const isRegistered = isDataShapeRegistered(dataShape, courseConfig.value);
181
+ const hasSchema = isDataShapeSchemaAvailable(dataShape, courseConfig.value);
182
+
183
+ if (!isRegistered) {
184
+ changes.push({
185
+ key: `ds-add-${dataShape.course}.${dataShape.name}`,
186
+ type: 'add',
187
+ label: `Add DataShape: ${dataShape.course}.${dataShape.name}`,
188
+ });
189
+ } else if (!hasSchema) {
190
+ changes.push({
191
+ key: `ds-update-${dataShape.course}.${dataShape.name}`,
192
+ type: 'update',
193
+ label: `Update DataShape schema: ${dataShape.course}.${dataShape.name}`,
194
+ });
195
+ }
196
+ }
197
+
198
+ // Check QuestionTypes
199
+ for (const question of processedQuestions.value) {
200
+ if (!isQuestionTypeRegistered(question, courseConfig.value)) {
201
+ changes.push({
202
+ key: `qt-add-${question.course}.${question.name}`,
203
+ type: 'add',
204
+ label: `Add QuestionType: ${question.name}`,
205
+ });
206
+ }
207
+ }
208
+
209
+ return changes;
210
+ });
211
+
212
+ const hasChanges = computed(() => changesList.value.length > 0);
213
+
214
+ // Methods
215
+ async function loadData() {
216
+ try {
217
+ loading.value = true;
218
+ error.value = null;
219
+
220
+ // Load custom questions
221
+ customQuestions.value = allCustomQuestions();
222
+
223
+ // Process custom questions
224
+ const processed = processCustomQuestionsData(customQuestions.value);
225
+ processedDataShapes.value = processed.dataShapes;
226
+ processedQuestions.value = processed.questions;
227
+
228
+ // Load course config
229
+ const courseId = config.course;
230
+ if (!courseId) {
231
+ throw new Error('No course ID configured in skuilder.config.json');
232
+ }
233
+
234
+ const courseDB = getDataLayer().getCourseDB(courseId);
235
+ courseConfig.value = await courseDB.getCourseConfig();
236
+ } catch (err) {
237
+ error.value = err instanceof Error ? err.message : String(err);
238
+ console.error('[AdminRegisterQuestions] Failed to load data:', err);
239
+ } finally {
240
+ loading.value = false;
241
+ }
242
+ }
243
+
244
+ async function registerQuestions() {
245
+ if (!customQuestions.value || !courseConfig.value) {
246
+ error.value = 'Missing data for registration';
247
+ return;
248
+ }
249
+
250
+ try {
251
+ registering.value = true;
252
+ error.value = null;
253
+ successMessage.value = null;
254
+
255
+ const authStore = useAuthStore();
256
+ const username = authStore.currentUser?.username || 'admin';
257
+
258
+ const courseId = config.course;
259
+ if (!courseId) {
260
+ throw new Error('No course ID configured');
261
+ }
262
+
263
+ const courseDB = getDataLayer().getCourseDB(courseId);
264
+
265
+ const result = await registerCustomQuestionTypes(
266
+ customQuestions.value,
267
+ courseConfig.value,
268
+ courseDB,
269
+ username
270
+ );
271
+
272
+ if (result.success) {
273
+ successMessage.value = `Successfully registered ${result.registeredCount} items`;
274
+ // Reload data to show updated state
275
+ await loadData();
276
+ } else {
277
+ error.value = result.errorMessage || 'Registration failed';
278
+ }
279
+ } catch (err) {
280
+ error.value = err instanceof Error ? err.message : String(err);
281
+ console.error('[AdminRegisterQuestions] Registration failed:', err);
282
+ } finally {
283
+ registering.value = false;
284
+ }
285
+ }
286
+
287
+ // Lifecycle
288
+ onMounted(() => {
289
+ if (!isStaticMode.value) {
290
+ loadData();
291
+ }
292
+ });
293
+ </script>
294
+
295
+ <style scoped>
296
+ .v-code {
297
+ background-color: rgba(0, 0, 0, 0.05);
298
+ padding: 8px 12px;
299
+ border-radius: 4px;
300
+ font-family: 'Courier New', monospace;
301
+ }
302
+
303
+ :deep(.v-theme--dark) .v-code {
304
+ background-color: rgba(255, 255, 255, 0.05);
305
+ }
306
+ </style>
@@ -125,7 +125,7 @@ const vuetify = createVuetify({
125
125
  const courseDB = getDataLayer().getCourseDB(studioConfig.database.name);
126
126
  const courseConfig = await courseDB.getCourseConfig();
127
127
 
128
- const { registerCustomQuestionTypes } = await import('./utils/courseConfigRegistration');
128
+ const { registerCustomQuestionTypes } = await import('@vue-skuilder/db');
129
129
  const registrationResult = await registerCustomQuestionTypes(
130
130
  customQuestions,
131
131
  courseConfig,
@@ -156,7 +156,7 @@ const vuetify = createVuetify({
156
156
  const courseConfig = await courseDB.getCourseConfig();
157
157
 
158
158
  const { BlanksCard, BlanksCardDataShapes } = await import('@vue-skuilder/courseware');
159
- const { registerBlanksCard } = await import('./utils/courseConfigRegistration');
159
+ const { registerBlanksCard } = await import('@vue-skuilder/db');
160
160
 
161
161
  const blanksRegistrationResult = await registerBlanksCard(
162
162
  BlanksCard,
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.1.22",
6
+ "version": "0.1.23",
7
7
  "type": "module",
8
8
  "scripts": {
9
9
  "dev": "vite",
@@ -32,5 +32,5 @@
32
32
  "vite": "^7.0.0",
33
33
  "vue-tsc": "^1.8.0"
34
34
  },
35
- "stableVersion": "0.1.22"
35
+ "stableVersion": "0.1.23"
36
36
  }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.1.22",
6
+ "version": "0.1.23",
7
7
  "type": "module",
8
8
  "description": "CLI scaffolding tool for vue-skuilder projects",
9
9
  "bin": {
@@ -45,13 +45,13 @@
45
45
  ],
46
46
  "dependencies": {
47
47
  "@mdi/font": "^7.3.67",
48
- "@vue-skuilder/common": "0.1.22",
49
- "@vue-skuilder/common-ui": "0.1.22",
50
- "@vue-skuilder/courseware": "0.1.22",
51
- "@vue-skuilder/db": "0.1.22",
52
- "@vue-skuilder/edit-ui": "0.1.22",
53
- "@vue-skuilder/express": "0.1.22",
54
- "@vue-skuilder/mcp": "0.1.22",
48
+ "@vue-skuilder/common": "0.1.23",
49
+ "@vue-skuilder/common-ui": "0.1.23",
50
+ "@vue-skuilder/courseware": "0.1.23",
51
+ "@vue-skuilder/db": "0.1.23",
52
+ "@vue-skuilder/edit-ui": "0.1.23",
53
+ "@vue-skuilder/express": "0.1.23",
54
+ "@vue-skuilder/mcp": "0.1.23",
55
55
  "chalk": "^5.3.0",
56
56
  "commander": "^11.0.0",
57
57
  "fs-extra": "^11.2.0",
@@ -70,7 +70,7 @@
70
70
  "@types/node": "^22",
71
71
  "@types/serve-static": "^1.15.0",
72
72
  "@vitejs/plugin-vue": "^6.0.0",
73
- "@vue-skuilder/studio-ui": "0.1.22",
73
+ "@vue-skuilder/studio-ui": "0.1.23",
74
74
  "cypress": "^15.6.0",
75
75
  "typescript": "~5.9.3",
76
76
  "vite": "^7.0.0",
@@ -81,5 +81,5 @@
81
81
  "node": ">=18.0.0"
82
82
  },
83
83
  "packageManager": "yarn@4.12.0",
84
- "stableVersion": "0.1.22"
84
+ "stableVersion": "0.1.23"
85
85
  }
@@ -1,416 +0,0 @@
1
- import { CourseConfig, DataShape, NameSpacer, toZodJSON } from '@vue-skuilder/common';
2
- import { CourseDBInterface } from '@vue-skuilder/db';
3
- import { Displayable, getCurrentUser, ViewComponent } from '@vue-skuilder/common-ui';
4
- import { CourseWare } from '@vue-skuilder/courseware';
5
-
6
- /**
7
- * Interface for custom questions data structure returned by allCustomQuestions()
8
- */
9
- export interface CustomQuestionsData {
10
- courses: CourseWare[]; // Course instances with question instances
11
- questionClasses: Displayable[]; // Question class constructors
12
- dataShapes: DataShape[]; // DataShape definitions for studio-ui
13
- views: ViewComponent[]; // Vue components for rendering
14
- meta: {
15
- questionCount: number;
16
- dataShapeCount: number;
17
- viewCount: number;
18
- courseCount: number;
19
- packageName: string;
20
- sourceDirectory: string;
21
- };
22
- }
23
-
24
- /**
25
- * Interface for processed question data for registration
26
- */
27
- export interface ProcessedQuestionData {
28
- name: string;
29
- course: string;
30
- questionClass: any;
31
- dataShapes: any[];
32
- views: any[];
33
- }
34
-
35
- /**
36
- * Interface for processed data shape for registration
37
- */
38
- export interface ProcessedDataShape {
39
- name: string;
40
- course: string;
41
- dataShape: any;
42
- }
43
-
44
- /**
45
- * Check if a data shape is already registered in the course config with valid schema
46
- */
47
- export function isDataShapeRegistered(
48
- dataShape: ProcessedDataShape,
49
- courseConfig: CourseConfig
50
- ): boolean {
51
- const namespacedName = NameSpacer.getDataShapeString({
52
- dataShape: dataShape.name,
53
- course: dataShape.course,
54
- });
55
-
56
- const existingDataShape = courseConfig.dataShapes.find((ds) => ds.name === namespacedName);
57
-
58
- // existence sufficient to be considered "registered"
59
- return existingDataShape !== undefined;
60
- }
61
-
62
- export function isDataShapeSchemaAvailable(
63
- dataShape: ProcessedDataShape,
64
- courseConfig: CourseConfig
65
- ): boolean {
66
- const namespacedName = NameSpacer.getDataShapeString({
67
- dataShape: dataShape.name,
68
- course: dataShape.course,
69
- });
70
-
71
- const existingDataShape = courseConfig.dataShapes.find((ds) => ds.name === namespacedName);
72
-
73
- return existingDataShape !== undefined && existingDataShape.serializedZodSchema !== undefined;
74
- }
75
-
76
- /**
77
- * Check if a question type is already registered in the course config
78
- */
79
- export function isQuestionTypeRegistered(
80
- question: ProcessedQuestionData,
81
- courseConfig: CourseConfig
82
- ): boolean {
83
- const namespacedName = NameSpacer.getQuestionString({
84
- course: question.course,
85
- questionType: question.name,
86
- });
87
-
88
- return courseConfig.questionTypes.some((qt) => qt.name === namespacedName);
89
- }
90
-
91
- /**
92
- * Process custom questions data into registration-ready format
93
- */
94
- export function processCustomQuestionsData(customQuestions: CustomQuestionsData): {
95
- dataShapes: ProcessedDataShape[];
96
- questions: ProcessedQuestionData[];
97
- } {
98
- const processedDataShapes: ProcessedDataShape[] = [];
99
- const processedQuestions: ProcessedQuestionData[] = [];
100
-
101
- // Extract course names from the custom questions
102
- const courseNames = customQuestions.courses.map((course) => course.name);
103
-
104
- // Process each question class
105
- customQuestions.questionClasses.forEach((questionClass) => {
106
- // Determine the course name (use first course or default to meta.packageName)
107
- const courseName = courseNames.length > 0 ? courseNames[0] : customQuestions.meta.packageName;
108
-
109
- // Process data shapes from this question class (static property)
110
- if (questionClass.dataShapes && Array.isArray(questionClass.dataShapes)) {
111
- questionClass.dataShapes.forEach((dataShape) => {
112
- processedDataShapes.push({
113
- name: dataShape.name,
114
- course: courseName,
115
- dataShape: dataShape,
116
- });
117
- });
118
- }
119
-
120
- // Process the question itself
121
- processedQuestions.push({
122
- name: questionClass.name,
123
- course: courseName,
124
- questionClass: questionClass,
125
- dataShapes: questionClass.dataShapes || [],
126
- views: questionClass.views || [],
127
- });
128
- });
129
-
130
- return { dataShapes: processedDataShapes, questions: processedQuestions };
131
- }
132
-
133
- /**
134
- * Register a data shape in the course config
135
- */
136
- export function registerDataShape(
137
- dataShape: ProcessedDataShape,
138
- courseConfig: CourseConfig
139
- ): boolean {
140
- const namespacedName = NameSpacer.getDataShapeString({
141
- dataShape: dataShape.name,
142
- course: dataShape.course,
143
- });
144
-
145
- // Generate JSON Schema for the DataShape
146
- let serializedZodSchema: string | undefined;
147
- try {
148
- serializedZodSchema = toZodJSON(dataShape.dataShape);
149
- } catch (error) {
150
- console.warn(` ⚠️ Failed to generate schema for ${namespacedName}:`, error);
151
- serializedZodSchema = undefined;
152
- }
153
-
154
- // Check if DataShape already exists
155
- const existingIndex = courseConfig.dataShapes.findIndex((ds) => ds.name === namespacedName);
156
-
157
- if (existingIndex !== -1) {
158
- const existingDataShape = courseConfig.dataShapes[existingIndex];
159
-
160
- // If existing schema matches new schema, no update needed
161
- if (existingDataShape.serializedZodSchema === serializedZodSchema) {
162
- console.log(
163
- ` ℹ️ DataShape '${dataShape.name}' from '${dataShape.course}' already registered with identical schema`
164
- );
165
- return false;
166
- }
167
-
168
- // Schema has changed or was missing - update it
169
- if (existingDataShape.serializedZodSchema) {
170
- console.log(
171
- ` 🔄 DataShape '${dataShape.name}' from '${dataShape.course}' schema has changed, updating...`
172
- );
173
- } else {
174
- console.log(
175
- ` ℹ️ DataShape '${dataShape.name}' from '${dataShape.course}' already registered, but with no schema` +
176
- `\n ℹ️ Adding schema to existing entry`
177
- );
178
- }
179
-
180
- // Update the existing entry
181
- courseConfig.dataShapes[existingIndex] = {
182
- name: namespacedName,
183
- questionTypes: existingDataShape.questionTypes, // Preserve existing question type associations
184
- serializedZodSchema,
185
- };
186
-
187
- console.log(` ✅ Updated DataShape: ${namespacedName}`);
188
- return true;
189
- }
190
-
191
- courseConfig.dataShapes.push({
192
- name: namespacedName,
193
- questionTypes: [],
194
- serializedZodSchema,
195
- });
196
-
197
- console.log(` ✅ Registered DataShape: ${namespacedName}`);
198
- return true;
199
- }
200
-
201
- /**
202
- * Register a question type in the course config
203
- */
204
- export function registerQuestionType(
205
- question: ProcessedQuestionData,
206
- courseConfig: CourseConfig
207
- ): boolean {
208
- if (isQuestionTypeRegistered(question, courseConfig)) {
209
- console.log(
210
- ` ℹ️ QuestionType '${question.name}' from '${question.course}' already registered`
211
- );
212
- return false;
213
- }
214
-
215
- const namespacedQuestionName = NameSpacer.getQuestionString({
216
- course: question.course,
217
- questionType: question.name,
218
- });
219
-
220
- // Build view list
221
- const viewList = question.views.map((view) => {
222
- if (view.name) {
223
- return view.name;
224
- } else {
225
- return 'unnamedComponent';
226
- }
227
- });
228
-
229
- // Build data shape list
230
- const dataShapeList = question.dataShapes.map((dataShape) =>
231
- NameSpacer.getDataShapeString({
232
- course: question.course,
233
- dataShape: dataShape.name,
234
- })
235
- );
236
-
237
- // Add question type to course config
238
- courseConfig.questionTypes.push({
239
- name: namespacedQuestionName,
240
- viewList: viewList,
241
- dataShapeList: dataShapeList,
242
- });
243
-
244
- // Cross-reference: Add this question type to its data shapes
245
- question.dataShapes.forEach((dataShape) => {
246
- const namespacedDataShapeName = NameSpacer.getDataShapeString({
247
- course: question.course,
248
- dataShape: dataShape.name,
249
- });
250
-
251
- for (const ds of courseConfig.dataShapes) {
252
- if (ds.name === namespacedDataShapeName) {
253
- ds.questionTypes.push(namespacedQuestionName);
254
- }
255
- }
256
- });
257
-
258
- console.log(` ✅ Registered QuestionType: ${namespacedQuestionName}`);
259
- return true;
260
- }
261
-
262
- /**
263
- * Register seed data for a question type (similar to ComponentRegistration)
264
- */
265
- export async function registerSeedData(
266
- question: ProcessedQuestionData,
267
- courseDB: CourseDBInterface
268
- // courseName: string
269
- ): Promise<void> {
270
- if (question.questionClass.seedData && Array.isArray(question.questionClass.seedData)) {
271
- console.log(` 📦 Registering seed data for question: ${question.name}`);
272
-
273
- try {
274
- const currentUser = await getCurrentUser();
275
-
276
- question.questionClass.seedData.forEach((seedDataItem: unknown) => {
277
- if (question.dataShapes.length > 0) {
278
- courseDB.addNote(
279
- question.course,
280
- question.dataShapes[0],
281
- seedDataItem,
282
- currentUser.getUsername(),
283
- []
284
- );
285
- }
286
- });
287
-
288
- console.log(` ✅ Seed data registered for question: ${question.name}`);
289
- } catch (error) {
290
- console.warn(
291
- ` ⚠️ Failed to register seed data for question '${question.name}': ${error instanceof Error ? error.message : String(error)}`
292
- );
293
- }
294
- }
295
- }
296
-
297
- /**
298
- * Register BlanksCard (markdown fillIn) question type specifically
299
- */
300
- export async function registerBlanksCard(
301
- BlanksCard: any,
302
- BlanksCardDataShapes: any[],
303
- courseConfig: CourseConfig,
304
- courseDB: CourseDBInterface
305
- ): Promise<{ success: boolean; errorMessage?: string }> {
306
- try {
307
- console.log(' 📋 Registering BlanksCard data shapes and question type...');
308
-
309
- let registeredCount = 0;
310
- const courseName = 'default'; // BlanksCard comes from the default course
311
-
312
- // Register BlanksCard data shapes
313
- for (const dataShapeClass of BlanksCardDataShapes) {
314
- const processedDataShape: ProcessedDataShape = {
315
- name: dataShapeClass.name,
316
- course: courseName,
317
- dataShape: dataShapeClass,
318
- };
319
-
320
- if (registerDataShape(processedDataShape, courseConfig)) {
321
- registeredCount++;
322
- }
323
- }
324
-
325
- // Register BlanksCard question type
326
- const processedQuestion: ProcessedQuestionData = {
327
- name: BlanksCard.name,
328
- course: courseName,
329
- questionClass: BlanksCard,
330
- dataShapes: BlanksCardDataShapes,
331
- views: BlanksCard.views || [],
332
- };
333
-
334
- if (registerQuestionType(processedQuestion, courseConfig)) {
335
- registeredCount++;
336
- }
337
-
338
- // Update the course config in the database
339
- console.log(' 💾 Updating course configuration with BlanksCard...');
340
- const updateResult = await courseDB.updateCourseConfig(courseConfig);
341
-
342
- if (!updateResult.ok) {
343
- throw new Error(`Failed to update course config: ${JSON.stringify(updateResult)}`);
344
- }
345
-
346
- // Register seed data if BlanksCard has any
347
- await registerSeedData(processedQuestion, courseDB);
348
-
349
- console.log(` ✅ BlanksCard registration complete: ${registeredCount} items registered`);
350
-
351
- return { success: true };
352
- } catch (error) {
353
- const errorMessage = error instanceof Error ? error.message : String(error);
354
- console.error(` ❌ BlanksCard registration failed: ${errorMessage}`);
355
- return { success: false, errorMessage };
356
- }
357
- }
358
-
359
- /**
360
- * Main function to register all custom question types and data shapes
361
- */
362
- export async function registerCustomQuestionTypes(
363
- customQuestions: CustomQuestionsData,
364
- courseConfig: CourseConfig,
365
- courseDB: CourseDBInterface
366
- // courseName: string
367
- ): Promise<{ success: boolean; registeredCount: number; errorMessage?: string }> {
368
- try {
369
- console.log('🎨 Studio Mode: Beginning custom question registration');
370
- console.log(` 📊 Processing ${customQuestions.questionClasses.length} question classes`);
371
-
372
- const { dataShapes, questions } = processCustomQuestionsData(customQuestions);
373
-
374
- console.log(` 📊 Found ${dataShapes.length} data shapes and ${questions.length} questions`);
375
-
376
- let registeredCount = 0;
377
-
378
- // First, register all data shapes
379
- console.log(' 📋 Registering data shapes...');
380
- for (const dataShape of dataShapes) {
381
- if (registerDataShape(dataShape, courseConfig)) {
382
- registeredCount++;
383
- }
384
- }
385
-
386
- // Then, register all question types
387
- console.log(' 🔧 Registering question types...');
388
- for (const question of questions) {
389
- if (registerQuestionType(question, courseConfig)) {
390
- registeredCount++;
391
- }
392
- }
393
-
394
- // Update the course config in the database
395
- console.log(' 💾 Updating course configuration...');
396
- const updateResult = await courseDB.updateCourseConfig(courseConfig);
397
-
398
- if (!updateResult.ok) {
399
- throw new Error(`Failed to update course config: ${JSON.stringify(updateResult)}`);
400
- }
401
-
402
- // Register seed data for questions that have it
403
- console.log(' 🌱 Registering seed data...');
404
- for (const question of questions) {
405
- await registerSeedData(question, courseDB /*, courseName */);
406
- }
407
-
408
- console.log(` ✅ Custom question registration complete: ${registeredCount} items registered`);
409
-
410
- return { success: true, registeredCount };
411
- } catch (error) {
412
- const errorMessage = error instanceof Error ? error.message : String(error);
413
- console.error(` ❌ Custom question registration failed: ${errorMessage}`);
414
- return { success: false, registeredCount: 0, errorMessage };
415
- }
416
- }