@vue-skuilder/cli 0.1.22 → 0.1.24
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/dist/standalone-ui-template/package.json +2 -2
- package/dist/standalone-ui-template/src/router/index.ts +30 -0
- package/dist/standalone-ui-template/src/views/AdminRegisterQuestionsView.vue +494 -0
- package/dist/studio-ui-src/main.ts +2 -2
- package/dist/studio-ui-src/package.json +2 -2
- package/package.json +10 -10
- package/dist/studio-ui-src/utils/courseConfigRegistration.ts +0 -416
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.1.
|
|
6
|
+
"version": "0.1.24",
|
|
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.
|
|
61
|
+
"stableVersion": "0.1.24"
|
|
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,494 @@
|
|
|
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
|
+
<!-- Access Denied -->
|
|
9
|
+
<v-alert v-if="!isAuthorized" type="error" variant="tonal" class="ma-4">
|
|
10
|
+
<v-alert-title>Access Denied</v-alert-title>
|
|
11
|
+
<p class="mb-2">
|
|
12
|
+
This admin page requires authentication with an authorized account.
|
|
13
|
+
</p>
|
|
14
|
+
<p v-if="!authStore.isLoggedIn" class="mb-2">
|
|
15
|
+
Please <a href="#" @click.prevent="authStore.setLoginDialog(true)">log in</a> to continue.
|
|
16
|
+
</p>
|
|
17
|
+
<p v-else class="mb-2">
|
|
18
|
+
Your account ({{ currentUsername }}) does not have admin privileges.
|
|
19
|
+
</p>
|
|
20
|
+
</v-alert>
|
|
21
|
+
|
|
22
|
+
<!-- Static Mode Warning -->
|
|
23
|
+
<v-alert v-else-if="isStaticMode" type="warning" variant="tonal" class="ma-4">
|
|
24
|
+
<v-alert-title>Static Mode Detected</v-alert-title>
|
|
25
|
+
<p class="mb-2">
|
|
26
|
+
Question type registration is not available in static mode.
|
|
27
|
+
</p>
|
|
28
|
+
<p class="mb-2">
|
|
29
|
+
To register custom question types, use Studio mode:
|
|
30
|
+
</p>
|
|
31
|
+
<v-code tag="pre" class="text-caption">yarn studio</v-code>
|
|
32
|
+
<p class="mt-2">
|
|
33
|
+
Studio mode provides a development environment with full database access for registering question types.
|
|
34
|
+
</p>
|
|
35
|
+
</v-alert>
|
|
36
|
+
|
|
37
|
+
<!-- Loading State -->
|
|
38
|
+
<v-card-text v-else-if="isAuthorized && loading">
|
|
39
|
+
<v-progress-circular indeterminate color="primary" />
|
|
40
|
+
<span class="ml-4">Loading question type data...</span>
|
|
41
|
+
</v-card-text>
|
|
42
|
+
|
|
43
|
+
<!-- Error State -->
|
|
44
|
+
<v-alert v-else-if="error" type="error" variant="tonal" class="ma-4">
|
|
45
|
+
<v-alert-title>Error</v-alert-title>
|
|
46
|
+
{{ error }}
|
|
47
|
+
</v-alert>
|
|
48
|
+
|
|
49
|
+
<!-- Main Content -->
|
|
50
|
+
<v-card-text v-else>
|
|
51
|
+
<!-- Current State -->
|
|
52
|
+
<div class="mb-6">
|
|
53
|
+
<h3 class="text-h6 mb-2">Current CourseConfig</h3>
|
|
54
|
+
<v-expansion-panels variant="accordion">
|
|
55
|
+
<v-expansion-panel>
|
|
56
|
+
<v-expansion-panel-title>
|
|
57
|
+
<v-icon class="mr-2">mdi-shape</v-icon>
|
|
58
|
+
{{ currentDataShapeCount }} DataShapes registered
|
|
59
|
+
</v-expansion-panel-title>
|
|
60
|
+
<v-expansion-panel-text>
|
|
61
|
+
<v-list v-if="courseConfig?.dataShapes?.length" density="compact">
|
|
62
|
+
<v-list-item
|
|
63
|
+
v-for="ds in courseConfig.dataShapes"
|
|
64
|
+
:key="ds.name"
|
|
65
|
+
>
|
|
66
|
+
<template #default>
|
|
67
|
+
<v-list-item-title class="font-weight-medium">
|
|
68
|
+
{{ ds.name }}
|
|
69
|
+
</v-list-item-title>
|
|
70
|
+
<v-list-item-subtitle>
|
|
71
|
+
{{ ds.questionTypes?.length || 0 }} question types |
|
|
72
|
+
Schema: {{ ds.serializedZodSchema ? 'Yes' : 'No' }}
|
|
73
|
+
</v-list-item-subtitle>
|
|
74
|
+
</template>
|
|
75
|
+
<template #append>
|
|
76
|
+
<v-btn
|
|
77
|
+
icon="mdi-delete"
|
|
78
|
+
size="small"
|
|
79
|
+
variant="text"
|
|
80
|
+
color="error"
|
|
81
|
+
:loading="removing"
|
|
82
|
+
@click="confirmRemoveDataShape(ds.name)"
|
|
83
|
+
/>
|
|
84
|
+
</template>
|
|
85
|
+
</v-list-item>
|
|
86
|
+
</v-list>
|
|
87
|
+
<div v-else class="text-medium-emphasis pa-2">
|
|
88
|
+
No DataShapes registered
|
|
89
|
+
</div>
|
|
90
|
+
</v-expansion-panel-text>
|
|
91
|
+
</v-expansion-panel>
|
|
92
|
+
<v-expansion-panel>
|
|
93
|
+
<v-expansion-panel-title>
|
|
94
|
+
<v-icon class="mr-2">mdi-help-circle</v-icon>
|
|
95
|
+
{{ currentQuestionTypeCount }} QuestionTypes registered
|
|
96
|
+
</v-expansion-panel-title>
|
|
97
|
+
<v-expansion-panel-text>
|
|
98
|
+
<v-list v-if="courseConfig?.questionTypes?.length" density="compact">
|
|
99
|
+
<v-list-item
|
|
100
|
+
v-for="qt in courseConfig.questionTypes"
|
|
101
|
+
:key="qt.name"
|
|
102
|
+
>
|
|
103
|
+
<template #default>
|
|
104
|
+
<v-list-item-title class="font-weight-medium">
|
|
105
|
+
{{ qt.name }}
|
|
106
|
+
</v-list-item-title>
|
|
107
|
+
<v-list-item-subtitle>
|
|
108
|
+
Views: {{ qt.viewList?.join(', ') || 'none' }} |
|
|
109
|
+
DataShapes: {{ qt.dataShapeList?.join(', ') || 'none' }}
|
|
110
|
+
</v-list-item-subtitle>
|
|
111
|
+
</template>
|
|
112
|
+
<template #append>
|
|
113
|
+
<v-btn
|
|
114
|
+
icon="mdi-delete"
|
|
115
|
+
size="small"
|
|
116
|
+
variant="text"
|
|
117
|
+
color="error"
|
|
118
|
+
:loading="removing"
|
|
119
|
+
@click="confirmRemoveQuestionType(qt.name)"
|
|
120
|
+
/>
|
|
121
|
+
</template>
|
|
122
|
+
</v-list-item>
|
|
123
|
+
</v-list>
|
|
124
|
+
<div v-else class="text-medium-emphasis pa-2">
|
|
125
|
+
No QuestionTypes registered
|
|
126
|
+
</div>
|
|
127
|
+
</v-expansion-panel-text>
|
|
128
|
+
</v-expansion-panel>
|
|
129
|
+
</v-expansion-panels>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<!-- Custom Questions -->
|
|
133
|
+
<div class="mb-6">
|
|
134
|
+
<h3 class="text-h6 mb-2">From allCustomQuestions()</h3>
|
|
135
|
+
<v-expansion-panels variant="accordion">
|
|
136
|
+
<v-expansion-panel>
|
|
137
|
+
<v-expansion-panel-title>
|
|
138
|
+
<v-icon class="mr-2">mdi-shape</v-icon>
|
|
139
|
+
{{ processedDataShapes.length }} DataShapes found
|
|
140
|
+
</v-expansion-panel-title>
|
|
141
|
+
<v-expansion-panel-text>
|
|
142
|
+
<v-list v-if="processedDataShapes.length" density="compact">
|
|
143
|
+
<v-list-item
|
|
144
|
+
v-for="ds in processedDataShapes"
|
|
145
|
+
:key="`${ds.course}.${ds.name}`"
|
|
146
|
+
>
|
|
147
|
+
<v-list-item-title class="font-weight-medium">
|
|
148
|
+
{{ ds.course }}.{{ ds.name }}
|
|
149
|
+
</v-list-item-title>
|
|
150
|
+
<v-list-item-subtitle>
|
|
151
|
+
Fields: {{ ds.dataShape?.fields?.map(f => f.name).join(', ') || 'none' }}
|
|
152
|
+
</v-list-item-subtitle>
|
|
153
|
+
</v-list-item>
|
|
154
|
+
</v-list>
|
|
155
|
+
<div v-else class="text-medium-emphasis pa-2">
|
|
156
|
+
No DataShapes found in allCustomQuestions()
|
|
157
|
+
</div>
|
|
158
|
+
</v-expansion-panel-text>
|
|
159
|
+
</v-expansion-panel>
|
|
160
|
+
<v-expansion-panel>
|
|
161
|
+
<v-expansion-panel-title>
|
|
162
|
+
<v-icon class="mr-2">mdi-help-circle</v-icon>
|
|
163
|
+
{{ processedQuestions.length }} QuestionTypes found
|
|
164
|
+
</v-expansion-panel-title>
|
|
165
|
+
<v-expansion-panel-text>
|
|
166
|
+
<v-list v-if="processedQuestions.length" density="compact">
|
|
167
|
+
<v-list-item
|
|
168
|
+
v-for="q in processedQuestions"
|
|
169
|
+
:key="`${q.course}.${q.name}`"
|
|
170
|
+
>
|
|
171
|
+
<v-list-item-title class="font-weight-medium">
|
|
172
|
+
{{ q.course }}.{{ q.name }}
|
|
173
|
+
</v-list-item-title>
|
|
174
|
+
<v-list-item-subtitle>
|
|
175
|
+
DataShapes: {{ q.dataShapes?.map(ds => ds.name).join(', ') || 'none' }} |
|
|
176
|
+
Views: {{ q.views?.length || 0 }}
|
|
177
|
+
</v-list-item-subtitle>
|
|
178
|
+
</v-list-item>
|
|
179
|
+
</v-list>
|
|
180
|
+
<div v-else class="text-medium-emphasis pa-2">
|
|
181
|
+
No QuestionTypes found in allCustomQuestions()
|
|
182
|
+
</div>
|
|
183
|
+
</v-expansion-panel-text>
|
|
184
|
+
</v-expansion-panel>
|
|
185
|
+
</v-expansion-panels>
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
<!-- Changes Preview -->
|
|
189
|
+
<div v-if="hasChanges" class="mb-6">
|
|
190
|
+
<h3 class="text-h6 mb-2">Changes to Apply</h3>
|
|
191
|
+
<v-list density="compact">
|
|
192
|
+
<v-list-item
|
|
193
|
+
v-for="change in changesList"
|
|
194
|
+
:key="change.key"
|
|
195
|
+
:class="change.type === 'add' ? 'text-success' : 'text-info'"
|
|
196
|
+
>
|
|
197
|
+
<template #prepend>
|
|
198
|
+
<v-icon :color="change.type === 'add' ? 'success' : 'info'">
|
|
199
|
+
{{ change.type === 'add' ? 'mdi-plus' : 'mdi-update' }}
|
|
200
|
+
</v-icon>
|
|
201
|
+
</template>
|
|
202
|
+
<v-list-item-title>
|
|
203
|
+
{{ change.label }}
|
|
204
|
+
</v-list-item-title>
|
|
205
|
+
</v-list-item>
|
|
206
|
+
</v-list>
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
<!-- No Changes Message -->
|
|
210
|
+
<v-alert v-else type="info" variant="tonal">
|
|
211
|
+
All question types are already registered. No changes needed.
|
|
212
|
+
</v-alert>
|
|
213
|
+
|
|
214
|
+
<!-- Success Message -->
|
|
215
|
+
<v-alert v-if="successMessage" type="success" variant="tonal" class="mb-4">
|
|
216
|
+
<v-alert-title>Success</v-alert-title>
|
|
217
|
+
{{ successMessage }}
|
|
218
|
+
</v-alert>
|
|
219
|
+
</v-card-text>
|
|
220
|
+
|
|
221
|
+
<!-- Actions -->
|
|
222
|
+
<v-card-actions v-if="isAuthorized && !isStaticMode && !loading && !error">
|
|
223
|
+
<v-spacer />
|
|
224
|
+
<v-btn
|
|
225
|
+
color="primary"
|
|
226
|
+
:disabled="!hasChanges || registering"
|
|
227
|
+
:loading="registering"
|
|
228
|
+
@click="registerQuestions"
|
|
229
|
+
>
|
|
230
|
+
Register Question Types
|
|
231
|
+
</v-btn>
|
|
232
|
+
</v-card-actions>
|
|
233
|
+
</v-card>
|
|
234
|
+
</v-container>
|
|
235
|
+
</template>
|
|
236
|
+
|
|
237
|
+
<script setup lang="ts">
|
|
238
|
+
import { ref, computed, onMounted } from 'vue';
|
|
239
|
+
import { getDataLayer } from '@vue-skuilder/db';
|
|
240
|
+
import {
|
|
241
|
+
registerCustomQuestionTypes,
|
|
242
|
+
removeCustomQuestionTypes,
|
|
243
|
+
processCustomQuestionsData,
|
|
244
|
+
isDataShapeRegistered,
|
|
245
|
+
isQuestionTypeRegistered,
|
|
246
|
+
isDataShapeSchemaAvailable,
|
|
247
|
+
type CustomQuestionsData,
|
|
248
|
+
type ProcessedDataShape,
|
|
249
|
+
type ProcessedQuestionData,
|
|
250
|
+
} from '@vue-skuilder/db';
|
|
251
|
+
import { useAuthStore, getCurrentUser } from '@vue-skuilder/common-ui';
|
|
252
|
+
import { allCustomQuestions } from '../questions';
|
|
253
|
+
import config from '../../skuilder.config.json';
|
|
254
|
+
|
|
255
|
+
// Admin usernames that can access this page
|
|
256
|
+
const ADMIN_USERNAMES = ['admin'];
|
|
257
|
+
|
|
258
|
+
// Auth state
|
|
259
|
+
const authStore = useAuthStore();
|
|
260
|
+
const currentUsername = ref<string | null>(null);
|
|
261
|
+
|
|
262
|
+
// Check if user is authorized (logged in + admin username)
|
|
263
|
+
const isAuthorized = computed(() => {
|
|
264
|
+
if (!authStore.isLoggedIn) return false;
|
|
265
|
+
if (!currentUsername.value) return false;
|
|
266
|
+
return ADMIN_USERNAMES.includes(currentUsername.value);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// State
|
|
270
|
+
const loading = ref(true);
|
|
271
|
+
const error = ref<string | null>(null);
|
|
272
|
+
const registering = ref(false);
|
|
273
|
+
const removing = ref(false);
|
|
274
|
+
const successMessage = ref<string | null>(null);
|
|
275
|
+
|
|
276
|
+
const courseConfig = ref<any>(null);
|
|
277
|
+
const customQuestions = ref<CustomQuestionsData | null>(null);
|
|
278
|
+
const processedDataShapes = ref<ProcessedDataShape[]>([]);
|
|
279
|
+
const processedQuestions = ref<ProcessedQuestionData[]>([]);
|
|
280
|
+
|
|
281
|
+
// Check if in static mode
|
|
282
|
+
const isStaticMode = computed(() => config.dataLayerType === 'static');
|
|
283
|
+
|
|
284
|
+
// Computed properties
|
|
285
|
+
const currentDataShapeCount = computed(() => courseConfig.value?.dataShapes?.length ?? 0);
|
|
286
|
+
const currentQuestionTypeCount = computed(() => courseConfig.value?.questionTypes?.length ?? 0);
|
|
287
|
+
|
|
288
|
+
interface Change {
|
|
289
|
+
key: string;
|
|
290
|
+
type: 'add' | 'update';
|
|
291
|
+
label: string;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const changesList = computed<Change[]>(() => {
|
|
295
|
+
if (!courseConfig.value || !customQuestions.value) return [];
|
|
296
|
+
|
|
297
|
+
const changes: Change[] = [];
|
|
298
|
+
|
|
299
|
+
// Check DataShapes
|
|
300
|
+
for (const dataShape of processedDataShapes.value) {
|
|
301
|
+
const isRegistered = isDataShapeRegistered(dataShape, courseConfig.value);
|
|
302
|
+
const hasSchema = isDataShapeSchemaAvailable(dataShape, courseConfig.value);
|
|
303
|
+
|
|
304
|
+
if (!isRegistered) {
|
|
305
|
+
changes.push({
|
|
306
|
+
key: `ds-add-${dataShape.course}.${dataShape.name}`,
|
|
307
|
+
type: 'add',
|
|
308
|
+
label: `Add DataShape: ${dataShape.course}.${dataShape.name}`,
|
|
309
|
+
});
|
|
310
|
+
} else if (!hasSchema) {
|
|
311
|
+
changes.push({
|
|
312
|
+
key: `ds-update-${dataShape.course}.${dataShape.name}`,
|
|
313
|
+
type: 'update',
|
|
314
|
+
label: `Update DataShape schema: ${dataShape.course}.${dataShape.name}`,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Check QuestionTypes
|
|
320
|
+
for (const question of processedQuestions.value) {
|
|
321
|
+
if (!isQuestionTypeRegistered(question, courseConfig.value)) {
|
|
322
|
+
changes.push({
|
|
323
|
+
key: `qt-add-${question.course}.${question.name}`,
|
|
324
|
+
type: 'add',
|
|
325
|
+
label: `Add QuestionType: ${question.name}`,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return changes;
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const hasChanges = computed(() => changesList.value.length > 0);
|
|
334
|
+
|
|
335
|
+
// Methods
|
|
336
|
+
async function loadData() {
|
|
337
|
+
try {
|
|
338
|
+
loading.value = true;
|
|
339
|
+
error.value = null;
|
|
340
|
+
|
|
341
|
+
// Load custom questions
|
|
342
|
+
customQuestions.value = allCustomQuestions();
|
|
343
|
+
|
|
344
|
+
// Process custom questions
|
|
345
|
+
const processed = processCustomQuestionsData(customQuestions.value);
|
|
346
|
+
processedDataShapes.value = processed.dataShapes;
|
|
347
|
+
processedQuestions.value = processed.questions;
|
|
348
|
+
|
|
349
|
+
// Load course config
|
|
350
|
+
const courseId = config.course;
|
|
351
|
+
if (!courseId) {
|
|
352
|
+
throw new Error('No course ID configured in skuilder.config.json');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const courseDB = getDataLayer().getCourseDB(courseId);
|
|
356
|
+
courseConfig.value = await courseDB.getCourseConfig();
|
|
357
|
+
} catch (err) {
|
|
358
|
+
error.value = err instanceof Error ? err.message : String(err);
|
|
359
|
+
console.error('[AdminRegisterQuestions] Failed to load data:', err);
|
|
360
|
+
} finally {
|
|
361
|
+
loading.value = false;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function registerQuestions() {
|
|
366
|
+
if (!customQuestions.value || !courseConfig.value) {
|
|
367
|
+
error.value = 'Missing data for registration';
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
try {
|
|
372
|
+
registering.value = true;
|
|
373
|
+
error.value = null;
|
|
374
|
+
successMessage.value = null;
|
|
375
|
+
|
|
376
|
+
const authStore = useAuthStore();
|
|
377
|
+
const username = authStore.currentUser?.username || 'admin';
|
|
378
|
+
|
|
379
|
+
const courseId = config.course;
|
|
380
|
+
if (!courseId) {
|
|
381
|
+
throw new Error('No course ID configured');
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const courseDB = getDataLayer().getCourseDB(courseId);
|
|
385
|
+
|
|
386
|
+
const result = await registerCustomQuestionTypes(
|
|
387
|
+
customQuestions.value,
|
|
388
|
+
courseConfig.value,
|
|
389
|
+
courseDB,
|
|
390
|
+
username
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
if (result.success) {
|
|
394
|
+
successMessage.value = `Successfully registered ${result.registeredCount} items`;
|
|
395
|
+
// Reload data to show updated state
|
|
396
|
+
await loadData();
|
|
397
|
+
} else {
|
|
398
|
+
error.value = result.errorMessage || 'Registration failed';
|
|
399
|
+
}
|
|
400
|
+
} catch (err) {
|
|
401
|
+
error.value = err instanceof Error ? err.message : String(err);
|
|
402
|
+
console.error('[AdminRegisterQuestions] Registration failed:', err);
|
|
403
|
+
} finally {
|
|
404
|
+
registering.value = false;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async function confirmRemoveDataShape(name: string) {
|
|
409
|
+
if (!confirm(`Remove DataShape "${name}"? This cannot be undone.`)) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
await removeItems([name], []);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async function confirmRemoveQuestionType(name: string) {
|
|
416
|
+
if (!confirm(`Remove QuestionType "${name}"? This cannot be undone.`)) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
await removeItems([], [name]);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async function removeItems(dataShapeNames: string[], questionTypeNames: string[]) {
|
|
423
|
+
if (!courseConfig.value) {
|
|
424
|
+
error.value = 'Missing course config for removal';
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
removing.value = true;
|
|
430
|
+
error.value = null;
|
|
431
|
+
successMessage.value = null;
|
|
432
|
+
|
|
433
|
+
const courseId = config.course;
|
|
434
|
+
if (!courseId) {
|
|
435
|
+
throw new Error('No course ID configured');
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const courseDB = getDataLayer().getCourseDB(courseId);
|
|
439
|
+
|
|
440
|
+
const result = await removeCustomQuestionTypes(
|
|
441
|
+
dataShapeNames,
|
|
442
|
+
questionTypeNames,
|
|
443
|
+
courseConfig.value,
|
|
444
|
+
courseDB
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
if (result.success) {
|
|
448
|
+
successMessage.value = `Successfully removed ${result.removedCount} items`;
|
|
449
|
+
// Reload data to show updated state
|
|
450
|
+
await loadData();
|
|
451
|
+
} else {
|
|
452
|
+
error.value = result.errorMessage || 'Removal failed';
|
|
453
|
+
}
|
|
454
|
+
} catch (err) {
|
|
455
|
+
error.value = err instanceof Error ? err.message : String(err);
|
|
456
|
+
console.error('[AdminRegisterQuestions] Removal failed:', err);
|
|
457
|
+
} finally {
|
|
458
|
+
removing.value = false;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Lifecycle
|
|
463
|
+
onMounted(async () => {
|
|
464
|
+
// Check authorization first
|
|
465
|
+
try {
|
|
466
|
+
const user = await getCurrentUser();
|
|
467
|
+
if (user) {
|
|
468
|
+
currentUsername.value = user.getUsername();
|
|
469
|
+
}
|
|
470
|
+
} catch (err) {
|
|
471
|
+
console.error('[AdminRegisterQuestions] Failed to get current user:', err);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Only load data if authorized and not in static mode
|
|
475
|
+
if (isAuthorized.value && !isStaticMode.value) {
|
|
476
|
+
loadData();
|
|
477
|
+
} else {
|
|
478
|
+
loading.value = false;
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
</script>
|
|
482
|
+
|
|
483
|
+
<style scoped>
|
|
484
|
+
.v-code {
|
|
485
|
+
background-color: rgba(0, 0, 0, 0.05);
|
|
486
|
+
padding: 8px 12px;
|
|
487
|
+
border-radius: 4px;
|
|
488
|
+
font-family: 'Courier New', monospace;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
:deep(.v-theme--dark) .v-code {
|
|
492
|
+
background-color: rgba(255, 255, 255, 0.05);
|
|
493
|
+
}
|
|
494
|
+
</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('
|
|
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('
|
|
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.
|
|
6
|
+
"version": "0.1.24",
|
|
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.
|
|
35
|
+
"stableVersion": "0.1.24"
|
|
36
36
|
}
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.1.
|
|
6
|
+
"version": "0.1.24",
|
|
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.
|
|
49
|
-
"@vue-skuilder/common-ui": "0.1.
|
|
50
|
-
"@vue-skuilder/courseware": "0.1.
|
|
51
|
-
"@vue-skuilder/db": "0.1.
|
|
52
|
-
"@vue-skuilder/edit-ui": "0.1.
|
|
53
|
-
"@vue-skuilder/express": "0.1.
|
|
54
|
-
"@vue-skuilder/mcp": "0.1.
|
|
48
|
+
"@vue-skuilder/common": "0.1.24",
|
|
49
|
+
"@vue-skuilder/common-ui": "0.1.24",
|
|
50
|
+
"@vue-skuilder/courseware": "0.1.24",
|
|
51
|
+
"@vue-skuilder/db": "0.1.24",
|
|
52
|
+
"@vue-skuilder/edit-ui": "0.1.24",
|
|
53
|
+
"@vue-skuilder/express": "0.1.24",
|
|
54
|
+
"@vue-skuilder/mcp": "0.1.24",
|
|
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.
|
|
73
|
+
"@vue-skuilder/studio-ui": "0.1.24",
|
|
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.
|
|
84
|
+
"stableVersion": "0.1.24"
|
|
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
|
-
}
|