@vue-skuilder/edit-ui 0.1.1
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/assets/index.css +1 -0
- package/dist/edit-ui.es.js +71090 -0
- package/dist/edit-ui.es.js.map +1 -0
- package/dist/edit-ui.umd.js +83 -0
- package/dist/edit-ui.umd.js.map +1 -0
- package/package.json +67 -0
- package/src/components/BulkImport/CardPreviewList.vue +345 -0
- package/src/components/BulkImportView.vue +633 -0
- package/src/components/CourseEditor.vue +164 -0
- package/src/components/ViewableDataInputForm/DataInputForm.vue +533 -0
- package/src/components/ViewableDataInputForm/FieldInput.types.ts +33 -0
- package/src/components/ViewableDataInputForm/FieldInputs/AudioInput.vue +188 -0
- package/src/components/ViewableDataInputForm/FieldInputs/ChessPuzzleInput.vue +79 -0
- package/src/components/ViewableDataInputForm/FieldInputs/FieldInput.css +12 -0
- package/src/components/ViewableDataInputForm/FieldInputs/ImageInput.vue +231 -0
- package/src/components/ViewableDataInputForm/FieldInputs/IntegerInput.vue +49 -0
- package/src/components/ViewableDataInputForm/FieldInputs/MarkdownInput.vue +34 -0
- package/src/components/ViewableDataInputForm/FieldInputs/MediaDragDropUploader.vue +246 -0
- package/src/components/ViewableDataInputForm/FieldInputs/MidiInput.vue +113 -0
- package/src/components/ViewableDataInputForm/FieldInputs/NumberInput.vue +49 -0
- package/src/components/ViewableDataInputForm/FieldInputs/OptionsFieldInput.ts +161 -0
- package/src/components/ViewableDataInputForm/FieldInputs/StringInput.vue +49 -0
- package/src/components/ViewableDataInputForm/FieldInputs/typeValidators.ts +49 -0
- package/src/components/index.ts +21 -0
- package/src/index.ts +6 -0
- package/src/stores/useDataInputFormStore.ts +49 -0
- package/src/stores/useFieldInputStore.ts +191 -0
- package/src/vue-shims.d.ts +5 -0
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-container fluid>
|
|
3
|
+
<v-row v-if="!parsingComplete">
|
|
4
|
+
<v-col cols="12">
|
|
5
|
+
<v-textarea
|
|
6
|
+
v-model="bulkText"
|
|
7
|
+
label="Bulk Card Input"
|
|
8
|
+
placeholder="Paste card data here.
|
|
9
|
+
Separate cards with two consecutive '---' lines on their own lines.
|
|
10
|
+
Example:
|
|
11
|
+
Card 1 Question
|
|
12
|
+
{{blank}}
|
|
13
|
+
tags: tagA, tagB
|
|
14
|
+
elo: 1500
|
|
15
|
+
---
|
|
16
|
+
---
|
|
17
|
+
Card 2 Question
|
|
18
|
+
Another {{blank}}
|
|
19
|
+
elo: 1200
|
|
20
|
+
tags: tagC"
|
|
21
|
+
rows="15"
|
|
22
|
+
variant="outlined"
|
|
23
|
+
data-cy="bulk-import-textarea"
|
|
24
|
+
></v-textarea>
|
|
25
|
+
</v-col>
|
|
26
|
+
</v-row>
|
|
27
|
+
|
|
28
|
+
<!-- Card Parsing Summary Section -->
|
|
29
|
+
<v-row v-if="parsingComplete" class="mb-4">
|
|
30
|
+
<v-col cols="12" md="4">
|
|
31
|
+
<v-card border>
|
|
32
|
+
<v-card-title>Parsing Summary</v-card-title>
|
|
33
|
+
<v-card-text>
|
|
34
|
+
<p>
|
|
35
|
+
<strong>{{ parsedCards.length }}</strong> card(s) parsed and ready for import.
|
|
36
|
+
</p>
|
|
37
|
+
<div v-if="parsedCards.length > 0">
|
|
38
|
+
<strong>Tags Found:</strong>
|
|
39
|
+
<template v-if="uniqueTags.length > 0">
|
|
40
|
+
<v-chip v-for="tag in uniqueTags" :key="tag" class="mr-1 mb-1" size="small" label>
|
|
41
|
+
{{ tag }}
|
|
42
|
+
</v-chip>
|
|
43
|
+
</template>
|
|
44
|
+
<template v-else>
|
|
45
|
+
<span class="text--secondary">No unique tags identified across parsed cards.</span>
|
|
46
|
+
</template>
|
|
47
|
+
</div>
|
|
48
|
+
</v-card-text>
|
|
49
|
+
</v-card>
|
|
50
|
+
</v-col>
|
|
51
|
+
<v-col cols="12" md="8">
|
|
52
|
+
<card-preview-list
|
|
53
|
+
v-if="parsedCards.length > 0"
|
|
54
|
+
ref="cardPreviewList"
|
|
55
|
+
v-model:parsed-cards="parsedCards"
|
|
56
|
+
:data-shape="dataShape"
|
|
57
|
+
:view-components="cardViewComponents"
|
|
58
|
+
@edit-card="handleEditCard"
|
|
59
|
+
@delete-card="handleDeleteCard"
|
|
60
|
+
/>
|
|
61
|
+
</v-col>
|
|
62
|
+
</v-row>
|
|
63
|
+
|
|
64
|
+
<!-- Card Edit Dialog -->
|
|
65
|
+
<v-dialog v-model="showEditDialog" max-width="800px">
|
|
66
|
+
<v-card>
|
|
67
|
+
<v-card-title>Edit Card</v-card-title>
|
|
68
|
+
<v-card-text>
|
|
69
|
+
<v-form @submit.prevent="saveEditedCard" @keydown.esc="closeEditDialog">
|
|
70
|
+
<v-textarea
|
|
71
|
+
ref="markdownTextarea"
|
|
72
|
+
v-model="editedMarkdown"
|
|
73
|
+
label="Card Content"
|
|
74
|
+
rows="6"
|
|
75
|
+
auto-grow
|
|
76
|
+
variant="outlined"
|
|
77
|
+
@keydown.ctrl.enter="saveEditedCard"
|
|
78
|
+
></v-textarea>
|
|
79
|
+
|
|
80
|
+
<div class="my-4">
|
|
81
|
+
<div class="d-flex align-center mb-2">
|
|
82
|
+
<h3 class="text-subtitle-1 mr-2">Tags</h3>
|
|
83
|
+
<v-text-field
|
|
84
|
+
v-model="newTagText"
|
|
85
|
+
density="compact"
|
|
86
|
+
hide-details
|
|
87
|
+
placeholder="Add a tag"
|
|
88
|
+
variant="outlined"
|
|
89
|
+
class="mr-2"
|
|
90
|
+
@keydown.enter.prevent="addTag"
|
|
91
|
+
@keydown.esc="closeEditDialog"
|
|
92
|
+
></v-text-field>
|
|
93
|
+
<v-btn size="small" color="primary" variant="text" @click="addTag">Add</v-btn>
|
|
94
|
+
</div>
|
|
95
|
+
<div class="d-flex flex-wrap">
|
|
96
|
+
<v-chip v-for="tag in editedTags" :key="tag" closable class="mr-1 mb-1" @click:close="removeTag(tag)">
|
|
97
|
+
{{ tag }}
|
|
98
|
+
</v-chip>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<v-text-field
|
|
103
|
+
v-model.number="editedElo"
|
|
104
|
+
label="ELO Rating (optional)"
|
|
105
|
+
type="number"
|
|
106
|
+
variant="outlined"
|
|
107
|
+
min="0"
|
|
108
|
+
max="3000"
|
|
109
|
+
></v-text-field>
|
|
110
|
+
</v-form>
|
|
111
|
+
</v-card-text>
|
|
112
|
+
<v-card-actions>
|
|
113
|
+
<v-spacer></v-spacer>
|
|
114
|
+
<v-btn color="grey" variant="text" @click="closeEditDialog">
|
|
115
|
+
Cancel
|
|
116
|
+
<v-tooltip activator="parent" location="top">Press ESC</v-tooltip>
|
|
117
|
+
</v-btn>
|
|
118
|
+
<v-btn color="primary" @click="saveEditedCard">
|
|
119
|
+
Save
|
|
120
|
+
<v-tooltip activator="parent" location="top">Press Ctrl+Enter</v-tooltip>
|
|
121
|
+
</v-btn>
|
|
122
|
+
</v-card-actions>
|
|
123
|
+
</v-card>
|
|
124
|
+
</v-dialog>
|
|
125
|
+
|
|
126
|
+
<v-row>
|
|
127
|
+
<v-col cols="12">
|
|
128
|
+
<!-- Button for initial parsing -->
|
|
129
|
+
<v-btn
|
|
130
|
+
v-if="!parsingComplete"
|
|
131
|
+
color="primary"
|
|
132
|
+
:loading="processing"
|
|
133
|
+
:disabled="!bulkText.trim() || processing"
|
|
134
|
+
data-cy="bulk-import-parse-btn"
|
|
135
|
+
@click="handleInitialParse"
|
|
136
|
+
>
|
|
137
|
+
Parse Cards
|
|
138
|
+
<v-icon end>mdi-play-circle-outline</v-icon>
|
|
139
|
+
</v-btn>
|
|
140
|
+
|
|
141
|
+
<!-- Buttons for post-parsing stage -->
|
|
142
|
+
<template v-if="parsingComplete">
|
|
143
|
+
<v-btn
|
|
144
|
+
color="primary"
|
|
145
|
+
class="mr-2"
|
|
146
|
+
:loading="processing"
|
|
147
|
+
:disabled="parsedCards.length === 0 || processing || importAttempted"
|
|
148
|
+
data-cy="bulk-import-confirm-btn"
|
|
149
|
+
@click="confirmAndImportCards"
|
|
150
|
+
>
|
|
151
|
+
Confirm and Import {{ parsedCards.length }} Card(s)
|
|
152
|
+
<v-icon end>mdi-check-circle-outline</v-icon>
|
|
153
|
+
</v-btn>
|
|
154
|
+
<v-btn
|
|
155
|
+
v-if="!importAttempted"
|
|
156
|
+
variant="outlined"
|
|
157
|
+
color="grey-darken-1"
|
|
158
|
+
:disabled="processing"
|
|
159
|
+
data-cy="bulk-import-edit-again-btn"
|
|
160
|
+
@click="resetToInputStage"
|
|
161
|
+
>
|
|
162
|
+
<v-icon start>mdi-pencil</v-icon>
|
|
163
|
+
Back to bulk-editor.
|
|
164
|
+
</v-btn>
|
|
165
|
+
<v-btn
|
|
166
|
+
v-if="importAttempted"
|
|
167
|
+
variant="outlined"
|
|
168
|
+
color="blue-darken-1"
|
|
169
|
+
:disabled="processing"
|
|
170
|
+
data-cy="bulk-import-add-another-btn"
|
|
171
|
+
@click="startNewBulkImport"
|
|
172
|
+
>
|
|
173
|
+
<v-icon start>mdi-plus-circle-outline</v-icon>
|
|
174
|
+
Add Another Bulk Import
|
|
175
|
+
</v-btn>
|
|
176
|
+
</template>
|
|
177
|
+
</v-col>
|
|
178
|
+
</v-row>
|
|
179
|
+
<v-row v-if="results.length > 0">
|
|
180
|
+
<v-col cols="12">
|
|
181
|
+
<v-list density="compact">
|
|
182
|
+
<v-list-subheader>Import Results</v-list-subheader>
|
|
183
|
+
<v-list-item
|
|
184
|
+
v-for="(result, index) in results"
|
|
185
|
+
:key="index"
|
|
186
|
+
:class="{
|
|
187
|
+
'lime-lighten-5': result.status === 'success',
|
|
188
|
+
'red-lighten-5': result.status === 'error',
|
|
189
|
+
'force-dark-text': result.status === 'success' || result.status === 'error',
|
|
190
|
+
}"
|
|
191
|
+
>
|
|
192
|
+
<v-list-item-title>
|
|
193
|
+
<v-icon :color="result.status === 'success' ? 'green' : 'red'">
|
|
194
|
+
{{ result.status === 'success' ? 'mdi-check-circle' : 'mdi-alert-circle' }}
|
|
195
|
+
</v-icon>
|
|
196
|
+
Card {{ index + 1 }}
|
|
197
|
+
</v-list-item-title>
|
|
198
|
+
<v-list-item-subtitle>
|
|
199
|
+
<div v-if="result.message" class="ml-6">{{ result.message }}</div>
|
|
200
|
+
<div v-if="result.cardId" class="ml-6">ID: {{ result.cardId }}</div>
|
|
201
|
+
<details v-if="result.status === 'error' && result.originalText" class="ml-6">
|
|
202
|
+
<summary>Original Input</summary>
|
|
203
|
+
<pre style="white-space: pre-wrap; background-color: #f5f5f5; padding: 5px">{{
|
|
204
|
+
result.originalText
|
|
205
|
+
}}</pre>
|
|
206
|
+
</details>
|
|
207
|
+
</v-list-item-subtitle>
|
|
208
|
+
</v-list-item>
|
|
209
|
+
</v-list>
|
|
210
|
+
</v-col>
|
|
211
|
+
</v-row>
|
|
212
|
+
</v-container>
|
|
213
|
+
</template>
|
|
214
|
+
|
|
215
|
+
<script lang="ts">
|
|
216
|
+
import { defineComponent, PropType } from 'vue';
|
|
217
|
+
import {
|
|
218
|
+
CourseConfig,
|
|
219
|
+
DataShape,
|
|
220
|
+
Status,
|
|
221
|
+
NameSpacer,
|
|
222
|
+
ParsedCard,
|
|
223
|
+
parseBulkTextToCards,
|
|
224
|
+
isValidBulkFormat,
|
|
225
|
+
} from '@vue-skuilder/common';
|
|
226
|
+
import { BlanksCardDataShapes, allCourses } from '@vue-skuilder/courses';
|
|
227
|
+
import { ViewComponent, getCurrentUser, alertUser } from '@vue-skuilder/common-ui';
|
|
228
|
+
import {
|
|
229
|
+
getDataLayer,
|
|
230
|
+
CourseDBInterface,
|
|
231
|
+
ImportResult,
|
|
232
|
+
importParsedCards,
|
|
233
|
+
validateProcessorConfig,
|
|
234
|
+
} from '@vue-skuilder/db';
|
|
235
|
+
import CardPreviewList from './BulkImport/CardPreviewList.vue';
|
|
236
|
+
|
|
237
|
+
export default defineComponent({
|
|
238
|
+
name: 'BulkImportView',
|
|
239
|
+
components: {
|
|
240
|
+
CardPreviewList,
|
|
241
|
+
},
|
|
242
|
+
props: {
|
|
243
|
+
courseCfg: {
|
|
244
|
+
type: Object as PropType<CourseConfig>,
|
|
245
|
+
required: true,
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
data() {
|
|
249
|
+
return {
|
|
250
|
+
bulkText: '',
|
|
251
|
+
parsedCards: [] as ParsedCard[],
|
|
252
|
+
parsingComplete: false,
|
|
253
|
+
importAttempted: false,
|
|
254
|
+
results: [] as ImportResult[],
|
|
255
|
+
processing: false, // Will be used for both parsing and import stages
|
|
256
|
+
courseDB: null as CourseDBInterface | null,
|
|
257
|
+
dataShape: BlanksCardDataShapes[0],
|
|
258
|
+
cardViewComponents: [] as ViewComponent[],
|
|
259
|
+
editingCard: null as ParsedCard | null,
|
|
260
|
+
editingCardIndex: -1,
|
|
261
|
+
showEditDialog: false,
|
|
262
|
+
editedMarkdown: '',
|
|
263
|
+
editedTags: [] as string[],
|
|
264
|
+
editedElo: undefined as number | undefined,
|
|
265
|
+
newTagText: '',
|
|
266
|
+
};
|
|
267
|
+
},
|
|
268
|
+
computed: {
|
|
269
|
+
uniqueTags(): string[] {
|
|
270
|
+
if (!this.parsedCards || this.parsedCards.length === 0) {
|
|
271
|
+
return [];
|
|
272
|
+
}
|
|
273
|
+
const allTags = this.parsedCards.reduce((acc, card) => {
|
|
274
|
+
if (card.tags && card.tags.length > 0) {
|
|
275
|
+
acc.push(...card.tags);
|
|
276
|
+
}
|
|
277
|
+
return acc;
|
|
278
|
+
}, [] as string[]);
|
|
279
|
+
return [...new Set(allTags)].sort();
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
created() {
|
|
283
|
+
if (this.courseCfg?.courseID) {
|
|
284
|
+
this.courseDB = getDataLayer().getCourseDB(this.courseCfg.courseID);
|
|
285
|
+
|
|
286
|
+
// Validate that we have datashapes in the course config
|
|
287
|
+
if (!this.courseCfg.dataShapes || this.courseCfg.dataShapes.length === 0) {
|
|
288
|
+
console.error('[BulkImportView] Course config does not contain any dataShapes.');
|
|
289
|
+
alertUser({
|
|
290
|
+
text: 'Course configuration has no dataShapes. Bulk import may not work correctly.',
|
|
291
|
+
status: Status.warning,
|
|
292
|
+
});
|
|
293
|
+
} else {
|
|
294
|
+
// Get the data shape and view components for card preview
|
|
295
|
+
this.initializeCardPreviewComponents();
|
|
296
|
+
}
|
|
297
|
+
} else {
|
|
298
|
+
console.error('[BulkImportView] Course config or Course ID is missing.');
|
|
299
|
+
alertUser({
|
|
300
|
+
text: 'Course configuration is missing. Cannot initialize bulk import.',
|
|
301
|
+
status: Status.error,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
methods: {
|
|
306
|
+
handleDeleteCard(index: number) {
|
|
307
|
+
// The card is already removed from the parsedCards array via v-model
|
|
308
|
+
// This method can be used for additional processing if needed
|
|
309
|
+
console.log(`[BulkImportView] Card at index ${index} was deleted`);
|
|
310
|
+
|
|
311
|
+
// Show alert to confirm deletion
|
|
312
|
+
alertUser({
|
|
313
|
+
text: 'Card removed from import list',
|
|
314
|
+
status: Status.ok,
|
|
315
|
+
});
|
|
316
|
+
},
|
|
317
|
+
|
|
318
|
+
handleEditCard(card: ParsedCard, index: number) {
|
|
319
|
+
// Disable keyboard shortcuts while editing
|
|
320
|
+
if (this.$refs.cardPreviewList) {
|
|
321
|
+
(this.$refs.cardPreviewList as { toggleShortcuts: (enabled: boolean) => void }).toggleShortcuts(false);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
this.editingCard = { ...card }; // Create a copy
|
|
325
|
+
this.editingCardIndex = index;
|
|
326
|
+
this.editedMarkdown = card.markdown;
|
|
327
|
+
this.editedTags = [...card.tags];
|
|
328
|
+
this.editedElo = card.elo;
|
|
329
|
+
this.showEditDialog = true;
|
|
330
|
+
|
|
331
|
+
// Focus the text area after dialog opens
|
|
332
|
+
this.$nextTick(() => {
|
|
333
|
+
if (this.$refs.markdownTextarea) {
|
|
334
|
+
(this.$refs.markdownTextarea as { $el: HTMLElement }).$el.querySelector('textarea')?.focus();
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
},
|
|
338
|
+
|
|
339
|
+
saveEditedCard() {
|
|
340
|
+
if (this.editingCardIndex < 0 || !this.editingCard) return;
|
|
341
|
+
|
|
342
|
+
// Create updated card
|
|
343
|
+
const updatedCard: ParsedCard = {
|
|
344
|
+
markdown: this.editedMarkdown,
|
|
345
|
+
tags: this.editedTags,
|
|
346
|
+
elo: this.editedElo,
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
// Update the card in the array
|
|
350
|
+
const updatedCards = [...this.parsedCards];
|
|
351
|
+
updatedCards[this.editingCardIndex] = updatedCard;
|
|
352
|
+
this.parsedCards = updatedCards;
|
|
353
|
+
|
|
354
|
+
// Reset editing state
|
|
355
|
+
this.closeEditDialog();
|
|
356
|
+
|
|
357
|
+
// Show alert to confirm edit
|
|
358
|
+
alertUser({
|
|
359
|
+
text: 'Card updated successfully',
|
|
360
|
+
status: Status.ok,
|
|
361
|
+
});
|
|
362
|
+
},
|
|
363
|
+
|
|
364
|
+
closeEditDialog() {
|
|
365
|
+
this.showEditDialog = false;
|
|
366
|
+
this.editingCard = null;
|
|
367
|
+
this.editingCardIndex = -1;
|
|
368
|
+
this.editedMarkdown = '';
|
|
369
|
+
this.editedTags = [];
|
|
370
|
+
this.editedElo = undefined;
|
|
371
|
+
|
|
372
|
+
// Re-enable keyboard shortcuts after editing
|
|
373
|
+
setTimeout(() => {
|
|
374
|
+
if (this.$refs.cardPreviewList) {
|
|
375
|
+
(this.$refs.cardPreviewList as { toggleShortcuts: (enabled: boolean) => void }).toggleShortcuts(true);
|
|
376
|
+
}
|
|
377
|
+
}, 100);
|
|
378
|
+
},
|
|
379
|
+
|
|
380
|
+
addTag() {
|
|
381
|
+
const newTag = this.newTagText.trim();
|
|
382
|
+
if (newTag && !this.editedTags.includes(newTag)) {
|
|
383
|
+
this.editedTags.push(newTag);
|
|
384
|
+
this.newTagText = '';
|
|
385
|
+
}
|
|
386
|
+
},
|
|
387
|
+
|
|
388
|
+
removeTag(tag: string) {
|
|
389
|
+
this.editedTags = this.editedTags.filter((t) => t !== tag);
|
|
390
|
+
},
|
|
391
|
+
|
|
392
|
+
initializeCardPreviewComponents() {
|
|
393
|
+
// Use the first data shape from the course config
|
|
394
|
+
const configDataShape = this.courseCfg?.dataShapes?.[0];
|
|
395
|
+
if (!configDataShape) return;
|
|
396
|
+
|
|
397
|
+
// Validate that we're using the correct dataShape for consistency with the import process
|
|
398
|
+
if (this.dataShape.name !== BlanksCardDataShapes[0].name) {
|
|
399
|
+
this.dataShape = BlanksCardDataShapes[0];
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Get the code course from the dataShape descriptor
|
|
403
|
+
const descriptor = NameSpacer.getDataShapeDescriptor(configDataShape.name);
|
|
404
|
+
const course = allCourses.getCourse(descriptor.course);
|
|
405
|
+
|
|
406
|
+
if (course) {
|
|
407
|
+
// Get view components for this dataShape
|
|
408
|
+
this.cardViewComponents = [];
|
|
409
|
+
|
|
410
|
+
// Add base question type views
|
|
411
|
+
course.getBaseQTypes().forEach((qType) => {
|
|
412
|
+
if (qType.dataShapes[0].name === this.dataShape?.name) {
|
|
413
|
+
this.cardViewComponents = this.cardViewComponents.concat(qType.views);
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
// Add question-specific views
|
|
418
|
+
for (const q of configDataShape.questionTypes) {
|
|
419
|
+
const qDescriptor = NameSpacer.getQuestionDescriptor(q);
|
|
420
|
+
const questionViews = course.getQuestion(qDescriptor.questionType)?.views || [];
|
|
421
|
+
this.cardViewComponents = this.cardViewComponents.concat(questionViews);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
console.log(`[BulkImportView] Loaded ${this.cardViewComponents.length} view components for card preview`);
|
|
425
|
+
}
|
|
426
|
+
},
|
|
427
|
+
|
|
428
|
+
resetToInputStage() {
|
|
429
|
+
this.parsingComplete = false;
|
|
430
|
+
this.parsedCards = [];
|
|
431
|
+
this.importAttempted = false; // Reset import attempt flag
|
|
432
|
+
// Optionally keep results if you want to show them even after going back
|
|
433
|
+
// this.results = [];
|
|
434
|
+
// this.bulkText = ''; // Optionally clear the bulk text
|
|
435
|
+
},
|
|
436
|
+
|
|
437
|
+
startNewBulkImport() {
|
|
438
|
+
this.bulkText = '';
|
|
439
|
+
this.results = [];
|
|
440
|
+
this.parsedCards = [];
|
|
441
|
+
this.parsingComplete = false;
|
|
442
|
+
this.importAttempted = false;
|
|
443
|
+
},
|
|
444
|
+
|
|
445
|
+
handleInitialParse() {
|
|
446
|
+
if (!this.courseDB) {
|
|
447
|
+
alertUser({
|
|
448
|
+
text: 'Database connection not available. Cannot process cards.',
|
|
449
|
+
status: Status.error,
|
|
450
|
+
});
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// isValidBulkFormat calls alertUser internally if format is invalid.
|
|
455
|
+
if (!isValidBulkFormat(this.bulkText)) {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
this.processing = true;
|
|
460
|
+
this.results = []; // Clear previous import results
|
|
461
|
+
this.parsedCards = []; // Clear previously parsed cards
|
|
462
|
+
this.parsingComplete = false; // Reset parsing complete state
|
|
463
|
+
|
|
464
|
+
try {
|
|
465
|
+
this.parsedCards = parseBulkTextToCards(this.bulkText);
|
|
466
|
+
|
|
467
|
+
if (this.parsedCards.length === 0) {
|
|
468
|
+
alertUser({
|
|
469
|
+
text: 'No cards could be parsed from the input. Please check the format and ensure cards are separated by two "---" lines and that cards have content.',
|
|
470
|
+
status: Status.warning,
|
|
471
|
+
});
|
|
472
|
+
this.processing = false;
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Initialize card preview components if not already done
|
|
477
|
+
if (this.cardViewComponents.length === 0) {
|
|
478
|
+
this.initializeCardPreviewComponents();
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Successfully parsed, ready for review stage
|
|
482
|
+
this.parsingComplete = true;
|
|
483
|
+
console.log(`[BulkImportView] Successfully parsed ${this.parsedCards.length} cards for preview`);
|
|
484
|
+
} catch (error) {
|
|
485
|
+
console.error('[BulkImportView] Error parsing bulk text:', error);
|
|
486
|
+
alertUser({
|
|
487
|
+
text: `Error parsing cards: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
488
|
+
status: Status.error,
|
|
489
|
+
});
|
|
490
|
+
} finally {
|
|
491
|
+
this.processing = false;
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
|
|
495
|
+
async confirmAndImportCards() {
|
|
496
|
+
if (!this.courseDB) {
|
|
497
|
+
alertUser({ text: 'Database connection lost before import.', status: Status.error });
|
|
498
|
+
this.processing = false; // Ensure processing is false
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (this.parsedCards.length === 0) {
|
|
502
|
+
alertUser({ text: 'No parsed cards to import.', status: Status.warning });
|
|
503
|
+
this.processing = false; // Ensure processing is false
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Validate that we have datashapes in the course config
|
|
508
|
+
if (!this.courseCfg?.dataShapes || this.courseCfg.dataShapes.length === 0) {
|
|
509
|
+
alertUser({
|
|
510
|
+
text: 'This course has no data shapes configured. Cannot import cards.',
|
|
511
|
+
status: Status.error,
|
|
512
|
+
});
|
|
513
|
+
this.processing = false;
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
this.processing = true;
|
|
518
|
+
this.results = []; // Clear results from parsing stage or previous attempts
|
|
519
|
+
|
|
520
|
+
const currentUser = await getCurrentUser();
|
|
521
|
+
const userName = currentUser.getUsername();
|
|
522
|
+
|
|
523
|
+
const dataShapeToUse: DataShape = BlanksCardDataShapes[0];
|
|
524
|
+
|
|
525
|
+
if (!dataShapeToUse) {
|
|
526
|
+
alertUser({ text: 'Critical: Could not find BlanksCardDataShapes. Aborting import.', status: Status.error });
|
|
527
|
+
this.processing = false;
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const configDataShape = this.courseCfg?.dataShapes?.[0];
|
|
532
|
+
if (!configDataShape) {
|
|
533
|
+
alertUser({
|
|
534
|
+
text: 'Critical: No data shapes found in course configuration. Aborting import.',
|
|
535
|
+
status: Status.error,
|
|
536
|
+
});
|
|
537
|
+
this.processing = false;
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const codeCourse = NameSpacer.getDataShapeDescriptor(configDataShape.name).course;
|
|
542
|
+
|
|
543
|
+
const processorConfig = {
|
|
544
|
+
dataShape: dataShapeToUse,
|
|
545
|
+
courseCode: codeCourse,
|
|
546
|
+
userName: userName,
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
const validation = validateProcessorConfig(processorConfig);
|
|
550
|
+
if (!validation.isValid) {
|
|
551
|
+
alertUser({
|
|
552
|
+
text: validation.errorMessage || 'Invalid processor configuration for import.',
|
|
553
|
+
status: Status.error,
|
|
554
|
+
});
|
|
555
|
+
this.processing = false;
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
console.log('[BulkImportView] Starting import of parsed cards:', {
|
|
560
|
+
courseID: this.courseCfg.courseID,
|
|
561
|
+
dataShapeToUse: dataShapeToUse.name,
|
|
562
|
+
courseCode: codeCourse,
|
|
563
|
+
numberOfCards: this.parsedCards.length,
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
try {
|
|
567
|
+
this.results = await importParsedCards(this.parsedCards, this.courseDB, processorConfig);
|
|
568
|
+
} catch (error) {
|
|
569
|
+
console.error('[BulkImportView] Error importing parsed cards:', error);
|
|
570
|
+
this.results.push({
|
|
571
|
+
originalText: 'Bulk Operation Error',
|
|
572
|
+
status: 'error',
|
|
573
|
+
message: `Critical error during import: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
574
|
+
});
|
|
575
|
+
} finally {
|
|
576
|
+
this.processing = false;
|
|
577
|
+
this.importAttempted = true; // Mark that an import attempt has been made
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (this.results.every((r) => r.status === 'success') && this.results.length > 0) {
|
|
581
|
+
// All successful, optionally reset
|
|
582
|
+
// this.bulkText = ''; // Clear input text
|
|
583
|
+
// this.parsingComplete = false; // Go back to input stage
|
|
584
|
+
// this.parsedCards = [];
|
|
585
|
+
alertUser({ text: `${this.results.length} card(s) imported successfully!`, status: Status.ok });
|
|
586
|
+
} else if (this.results.some((r) => r.status === 'error')) {
|
|
587
|
+
alertUser({ text: 'Some cards failed to import. Please review the results below.', status: Status.warning });
|
|
588
|
+
}
|
|
589
|
+
},
|
|
590
|
+
},
|
|
591
|
+
});
|
|
592
|
+
</script>
|
|
593
|
+
|
|
594
|
+
<style scoped>
|
|
595
|
+
.lime-lighten-5 {
|
|
596
|
+
background-color: #f9fbe7 !important; /* Vuetify's lime lighten-5 */
|
|
597
|
+
}
|
|
598
|
+
.red-lighten-5 {
|
|
599
|
+
background-color: #ffebee !important; /* Vuetify's red lighten-5 */
|
|
600
|
+
}
|
|
601
|
+
pre {
|
|
602
|
+
white-space: pre-wrap;
|
|
603
|
+
word-wrap: break-word;
|
|
604
|
+
background-color: #f5f5f5;
|
|
605
|
+
padding: 10px;
|
|
606
|
+
border-radius: 4px;
|
|
607
|
+
margin-top: 5px;
|
|
608
|
+
}
|
|
609
|
+
.force-dark-text {
|
|
610
|
+
color: rgba(0, 0, 0, 0.87) !important;
|
|
611
|
+
}
|
|
612
|
+
/* Ensure child elements also get dark text if not overridden */
|
|
613
|
+
.force-dark-text .v-list-item-subtitle,
|
|
614
|
+
.force-dark-text .v-list-item-title,
|
|
615
|
+
.force-dark-text div, /* Ensure divs within the list item also get dark text */
|
|
616
|
+
.force-dark-text summary {
|
|
617
|
+
/* Ensure summary elements for <details> also get dark text */
|
|
618
|
+
color: rgba(0, 0, 0, 0.87) !important;
|
|
619
|
+
}
|
|
620
|
+
/* Icons are handled by their :color prop, so no specific override needed here unless that changes. */
|
|
621
|
+
|
|
622
|
+
/* Card Preview Styling */
|
|
623
|
+
.card-preview-container {
|
|
624
|
+
border-radius: 4px;
|
|
625
|
+
max-width: 100%;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
@media (min-width: 960px) {
|
|
629
|
+
.card-preview-container {
|
|
630
|
+
max-width: 65%;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
</style>
|