Skip to content

Commit 3a4cf8c

Browse files
committed
add a bulk-input component
1 parent 554e9ba commit 3a4cf8c

File tree

1 file changed

+272
-0
lines changed

1 file changed

+272
-0
lines changed
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
<template>
2+
<v-container fluid>
3+
<v-row>
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+
---
15+
---
16+
Card 2 Question
17+
Another {{blank}}
18+
tags: tagC"
19+
rows="15"
20+
varient="outlined"
21+
data-cy="bulk-import-textarea"
22+
></v-textarea>
23+
</v-col>
24+
</v-row>
25+
<v-row>
26+
<v-col cols="12">
27+
<v-btn
28+
color="primary"
29+
:loading="processing"
30+
:disabled="!bulkText.trim()"
31+
data-cy="bulk-import-process-btn"
32+
@click="processCards"
33+
>
34+
Process Cards
35+
<v-icon end>mdi-play-circle-outline</v-icon>
36+
</v-btn>
37+
</v-col>
38+
</v-row>
39+
<v-row v-if="results.length > 0">
40+
<v-col cols="12">
41+
<v-list density="compact">
42+
<v-list-subheader>Import Results</v-list-subheader>
43+
<v-list-item
44+
v-for="(result, index) in results"
45+
:key="index"
46+
:class="{ 'lime-lighten-5': result.status === 'success', 'red-lighten-5': result.status === 'error' }"
47+
>
48+
<v-list-item-title>
49+
<v-icon :color="result.status === 'success' ? 'green' : 'red'">
50+
{{ result.status === 'success' ? 'mdi-check-circle' : 'mdi-alert-circle' }}
51+
</v-icon>
52+
Card {{ index + 1 }}
53+
</v-list-item-title>
54+
<v-list-item-subtitle>
55+
<div v-if="result.message" class="ml-6">{{ result.message }}</div>
56+
<div v-if="result.cardId" class="ml-6">ID: {{ result.cardId }}</div>
57+
<details v-if="result.status === 'error' && result.originalText" class="ml-6">
58+
<summary>Original Input</summary>
59+
<pre style="white-space: pre-wrap; background-color: #f5f5f5; padding: 5px">{{
60+
result.originalText
61+
}}</pre>
62+
</details>
63+
</v-list-item-subtitle>
64+
</v-list-item>
65+
</v-list>
66+
</v-col>
67+
</v-row>
68+
</v-container>
69+
</template>
70+
71+
<script lang="ts">
72+
import { defineComponent, PropType } from 'vue';
73+
import { CourseConfig, DataShape, Status } from '@vue-skuilder/common';
74+
import { BlanksCardDataShapes } from '@vue-skuilder/courses';
75+
import { getCurrentUser } from '@vue-skuilder/common-ui';
76+
import { getDataLayer, CourseDBInterface } from '@vue-skuilder/db';
77+
import { alertUser } from '@vue-skuilder/common-ui'; // For user feedback
78+
79+
interface ImportResult {
80+
originalText: string;
81+
status: 'success' | 'error';
82+
message: string;
83+
cardId?: string;
84+
}
85+
86+
interface ParsedCard {
87+
markdown: string;
88+
tags: string[];
89+
}
90+
91+
export default defineComponent({
92+
name: 'BulkImportView',
93+
props: {
94+
courseCfg: {
95+
type: Object as PropType<CourseConfig>,
96+
required: true,
97+
},
98+
},
99+
data() {
100+
return {
101+
bulkText: '',
102+
results: [] as ImportResult[],
103+
processing: false,
104+
courseDB: null as CourseDBInterface | null,
105+
};
106+
},
107+
created() {
108+
if (this.courseCfg?.courseID) {
109+
this.courseDB = getDataLayer().getCourseDB(this.courseCfg.courseID);
110+
} else {
111+
console.error('[BulkImportView] Course config or Course ID is missing.');
112+
alertUser({
113+
text: 'Course configuration is missing. Cannot initialize bulk import.',
114+
status: Status.error,
115+
});
116+
}
117+
},
118+
methods: {
119+
parseCard(cardString: string): ParsedCard | null {
120+
const trimmedCardString = cardString.trim();
121+
if (!trimmedCardString) {
122+
return null;
123+
}
124+
125+
const lines = trimmedCardString.split('\n');
126+
let tags: string[] = [];
127+
const markdownLines = [...lines];
128+
129+
if (lines.length > 0) {
130+
const lastLine = lines[lines.length - 1].trim();
131+
if (lastLine.toLowerCase().startsWith('tags:')) {
132+
tags = lastLine
133+
.substring(5)
134+
.split(',')
135+
.map((tag) => tag.trim())
136+
.filter((tag) => tag);
137+
markdownLines.pop(); // Remove the tags line
138+
}
139+
}
140+
141+
const markdown = markdownLines.join('\n').trim();
142+
if (!markdown) {
143+
// Card must have some markdown content
144+
return null;
145+
}
146+
return { markdown, tags };
147+
},
148+
149+
async processCards() {
150+
if (!this.courseDB) {
151+
alertUser({
152+
text: 'Database connection not available. Cannot process cards.',
153+
status: Status.error,
154+
});
155+
this.processing = false;
156+
return;
157+
}
158+
if (!this.bulkText.trim()) return;
159+
160+
this.processing = true;
161+
this.results = [];
162+
163+
const cardDelimiter = '\n---\n---\n';
164+
const cardStrings = this.bulkText.split(cardDelimiter);
165+
const currentUser = await getCurrentUser();
166+
const userName = currentUser.getUsername();
167+
const dataShapeToUse: DataShape = BlanksCardDataShapes[0];
168+
169+
if (!dataShapeToUse) {
170+
this.results.push({
171+
originalText: 'N/A - Configuration Error',
172+
status: 'error',
173+
message: 'Could not find BlanksCardDataShapes. Aborting.',
174+
});
175+
this.processing = false;
176+
return;
177+
}
178+
179+
for (const cardString of cardStrings) {
180+
const originalText = cardString.trim();
181+
if (!originalText) continue;
182+
183+
const parsed = this.parseCard(originalText);
184+
185+
if (!parsed) {
186+
this.results.push({
187+
originalText,
188+
status: 'error',
189+
message: 'Failed to parse card: Empty content after tag removal or invalid format.',
190+
});
191+
continue;
192+
}
193+
194+
const { markdown, tags } = parsed;
195+
196+
// The BlanksCardDataShapes expects an 'Input' field for markdown
197+
// and an 'Uploads' field for media.
198+
const cardData = {
199+
Input: markdown,
200+
Uploads: [], // As per requirement, no uploads for bulk import
201+
};
202+
203+
try {
204+
const result = await this.courseDB.addNote(
205+
'default',
206+
dataShapeToUse,
207+
cardData,
208+
userName,
209+
tags,
210+
undefined, // deck
211+
undefined // elo
212+
);
213+
214+
if (result.status === Status.ok) {
215+
this.results.push({
216+
originalText,
217+
status: 'success',
218+
message: 'Card added successfully.',
219+
cardId: '(unknown)',
220+
});
221+
} else {
222+
this.results.push({
223+
originalText,
224+
status: 'error',
225+
message: result.message || 'Failed to add card to database. Unknown error.',
226+
});
227+
}
228+
} catch (error) {
229+
console.error('Error adding note:', error);
230+
this.results.push({
231+
originalText,
232+
status: 'error',
233+
message: `Error adding card: ${(error as any).message || 'Unknown error'}`,
234+
});
235+
}
236+
}
237+
238+
if (this.results.length === 0 && cardStrings.length > 0 && cardStrings.every((s) => !s.trim())) {
239+
// This case handles if bulkText only contained delimiters or whitespace
240+
this.results.push({
241+
originalText: this.bulkText,
242+
status: 'error',
243+
message: 'No valid card data found. Please check your input and delimiters.',
244+
});
245+
}
246+
247+
this.processing = false;
248+
if (!this.results.some((r) => r.status === 'error')) {
249+
// Potentially clear bulkText if all successful, or offer a button to do so
250+
// this.bulkText = '';
251+
}
252+
},
253+
},
254+
});
255+
</script>
256+
257+
<style scoped>
258+
.lime-lighten-5 {
259+
background-color: #f9fbe7 !important; /* Vuetify's lime lighten-5 */
260+
}
261+
.red-lighten-5 {
262+
background-color: #ffebee !important; /* Vuetify's red lighten-5 */
263+
}
264+
pre {
265+
white-space: pre-wrap;
266+
word-wrap: break-word;
267+
background-color: #f5f5f5;
268+
padding: 10px;
269+
border-radius: 4px;
270+
margin-top: 5px;
271+
}
272+
</style>

0 commit comments

Comments
 (0)