Skip to content

Commit 4ec3d2f

Browse files
authored
bulk tidy (#695)
- **fix: userinput strings render inline...** - **refactor: bulk card processing logic to own files** - **add elo parsing to bulk-tags**
2 parents a997aae + d00a6bd commit 4ec3d2f

File tree

6 files changed

+482
-119
lines changed

6 files changed

+482
-119
lines changed

packages/common-ui/src/components/studentInputs/UserInputString.vue

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
<template>
2-
<v-container class="pa-0">
3-
<v-text-field
2+
<span class="user-input-container">
3+
<input
44
v-model="answer"
55
:autofocus="autofocus"
6-
:prepend-icon="prependIcon"
76
type="text"
8-
variant="underlined"
9-
single-line
10-
hide-details
7+
class="user-input-string"
8+
ref="input"
119
@keyup.enter="submitAnswer(answer)"
12-
></v-text-field>
13-
</v-container>
10+
/>
11+
</span>
1412
</template>
1513

1614
<script lang="ts">
@@ -61,3 +59,31 @@ export default defineComponent({
6159
},
6260
});
6361
</script>
62+
63+
<style scoped>
64+
.user-input-container {
65+
display: inline-block;
66+
min-width: 6em;
67+
vertical-align: baseline;
68+
}
69+
70+
.user-input-string {
71+
display: inline-block;
72+
background: transparent;
73+
border: none;
74+
border-bottom: 1px solid currentColor;
75+
color: currentColor;
76+
font-family: inherit;
77+
font-size: inherit;
78+
line-height: inherit;
79+
padding: 0;
80+
margin: 0;
81+
text-align: center;
82+
width: 100%;
83+
outline: none;
84+
}
85+
86+
.user-input-string:focus {
87+
border-bottom: 2px solid currentColor;
88+
}
89+
</style>

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

Lines changed: 43 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ Example:
1111
Card 1 Question
1212
{{blank}}
1313
tags: tagA, tagB
14+
elo: 1500
1415
---
1516
---
1617
Card 2 Question
1718
Another {{blank}}
19+
elo: 1200
1820
tags: tagC"
1921
rows="15"
2022
varient="outlined"
@@ -75,18 +77,7 @@ import { BlanksCardDataShapes } from '@vue-skuilder/courses';
7577
import { getCurrentUser } from '@vue-skuilder/common-ui';
7678
import { getDataLayer, CourseDBInterface } from '@vue-skuilder/db';
7779
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-
}
80+
import { ImportResult, processBulkCards, validateProcessorConfig, isValidBulkFormat } from '@/utils/bulkImport';
9081
9182
export default defineComponent({
9283
name: 'BulkImportView',
@@ -125,36 +116,6 @@ export default defineComponent({
125116
}
126117
},
127118
methods: {
128-
parseCard(cardString: string): ParsedCard | null {
129-
const trimmedCardString = cardString.trim();
130-
if (!trimmedCardString) {
131-
return null;
132-
}
133-
134-
const lines = trimmedCardString.split('\n');
135-
let tags: string[] = [];
136-
const markdownLines = [...lines];
137-
138-
if (lines.length > 0) {
139-
const lastLine = lines[lines.length - 1].trim();
140-
if (lastLine.toLowerCase().startsWith('tags:')) {
141-
tags = lastLine
142-
.substring(5)
143-
.split(',')
144-
.map((tag) => tag.trim())
145-
.filter((tag) => tag);
146-
markdownLines.pop(); // Remove the tags line
147-
}
148-
}
149-
150-
const markdown = markdownLines.join('\n').trim();
151-
if (!markdown) {
152-
// Card must have some markdown content
153-
return null;
154-
}
155-
return { markdown, tags };
156-
},
157-
158119
async processCards() {
159120
if (!this.courseDB) {
160121
alertUser({
@@ -164,7 +125,11 @@ export default defineComponent({
164125
this.processing = false;
165126
return;
166127
}
167-
if (!this.bulkText.trim()) return;
128+
129+
if (!isValidBulkFormat(this.bulkText)) {
130+
this.processing = false;
131+
return;
132+
}
168133
169134
// Validate that we have datashapes in the course config
170135
if (!this.courseCfg?.dataShapes || this.courseCfg.dataShapes.length === 0) {
@@ -179,8 +144,6 @@ export default defineComponent({
179144
this.processing = true;
180145
this.results = [];
181146
182-
const cardDelimiter = '\n---\n---\n';
183-
const cardStrings = this.bulkText.split(cardDelimiter);
184147
const currentUser = await getCurrentUser();
185148
const userName = currentUser.getUsername();
186149
@@ -205,80 +168,49 @@ export default defineComponent({
205168
dataShapeToUse: dataShapeToUse.name,
206169
});
207170
208-
for (const cardString of cardStrings) {
209-
const originalText = cardString.trim();
210-
if (!originalText) continue;
211-
212-
const parsed = this.parseCard(originalText);
213-
214-
if (!parsed) {
215-
this.results.push({
216-
originalText,
217-
status: 'error',
218-
message: 'Failed to parse card: Empty content after tag removal or invalid format.',
219-
});
220-
continue;
221-
}
222-
223-
const { markdown, tags } = parsed;
224-
225-
// The BlanksCardDataShapes expects an 'Input' field for markdown
226-
// and an 'Uploads' field for media.
227-
const cardData = {
228-
Input: markdown,
229-
Uploads: [], // As per requirement, no uploads for bulk import
230-
};
231-
232-
try {
233-
// Extract course code from first dataShape in course config
234-
const configDataShape = this.courseCfg?.dataShapes?.[0];
235-
if (!configDataShape) {
236-
throw new Error('No data shapes found in course configuration');
237-
}
171+
// Extract course code from first dataShape in course config
172+
const configDataShape = this.courseCfg?.dataShapes?.[0];
173+
if (!configDataShape) {
174+
this.results.push({
175+
originalText: 'N/A - Configuration Error',
176+
status: 'error',
177+
message: 'No data shapes found in course configuration',
178+
});
179+
this.processing = false;
180+
return;
181+
}
238182
239-
const codeCourse = NameSpacer.getDataShapeDescriptor(configDataShape.name).course;
240-
console.log(`[BulkImportView] Using codeCourse: ${codeCourse} for note addition`);
183+
const codeCourse = NameSpacer.getDataShapeDescriptor(configDataShape.name).course;
184+
console.log(`[BulkImportView] Using codeCourse: ${codeCourse} for note addition`);
241185
242-
const result = await this.courseDB.addNote(
243-
codeCourse,
244-
dataShapeToUse,
245-
cardData,
246-
userName,
247-
tags,
248-
undefined, // deck
249-
undefined // elo
250-
);
186+
// Prepare processor configuration
187+
const config = {
188+
dataShape: dataShapeToUse,
189+
courseCode: codeCourse,
190+
userName: userName,
191+
};
251192
252-
if (result.status === Status.ok) {
253-
this.results.push({
254-
originalText,
255-
status: 'success',
256-
message: 'Card added successfully.',
257-
cardId: result.id ? result.id : '(unknown)',
258-
});
259-
} else {
260-
this.results.push({
261-
originalText,
262-
status: 'error',
263-
message: result.message || 'Failed to add card to database. Unknown error.',
264-
});
265-
}
266-
} catch (error) {
267-
console.error('Error adding note:', error);
268-
this.results.push({
269-
originalText,
270-
status: 'error',
271-
message: `Error adding card: ${error instanceof Error ? error.message : 'Unknown error'}`,
272-
});
273-
}
193+
// Validate processor configuration
194+
const validation = validateProcessorConfig(config);
195+
if (!validation.isValid) {
196+
this.results.push({
197+
originalText: 'N/A - Configuration Error',
198+
status: 'error',
199+
message: validation.errorMessage || 'Invalid processor configuration',
200+
});
201+
this.processing = false;
202+
return;
274203
}
275204
276-
if (this.results.length === 0 && cardStrings.length > 0 && cardStrings.every((s) => !s.trim())) {
277-
// This case handles if bulkText only contained delimiters or whitespace
205+
// Process the cards
206+
try {
207+
this.results = await processBulkCards(this.bulkText, this.courseDB, config);
208+
} catch (error) {
209+
console.error('[BulkImportView] Error processing cards:', error);
278210
this.results.push({
279211
originalText: this.bulkText,
280212
status: 'error',
281-
message: 'No valid card data found. Please check your input and delimiters.',
213+
message: `Error processing cards: ${error instanceof Error ? error.message : 'Unknown error'}`,
282214
});
283215
}
284216
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { ParsedCard } from './types';
2+
3+
/**
4+
* Configuration for the bulk card parser
5+
*/
6+
export interface CardParserConfig {
7+
/** Custom tag identifier (defaults to 'tags:') */
8+
tagIdentifier?: string;
9+
/** Custom ELO identifier (defaults to 'elo:') */
10+
eloIdentifier?: string;
11+
}
12+
13+
/**
14+
* Default configuration for the card parser
15+
*/
16+
const DEFAULT_PARSER_CONFIG: CardParserConfig = {
17+
tagIdentifier: 'tags:',
18+
eloIdentifier: 'elo:',
19+
};
20+
21+
/**
22+
* Card delimiter used to separate cards in bulk input
23+
*/
24+
export const CARD_DELIMITER = '\n---\n---\n';
25+
26+
/**
27+
* Parses a single card string into a structured object
28+
*
29+
* @param cardString - Raw string containing card content
30+
* @param config - Optional parser configuration
31+
* @returns ParsedCard object or null if parsing fails
32+
*/
33+
export function parseCard(cardString: string, config: CardParserConfig = DEFAULT_PARSER_CONFIG): ParsedCard | null {
34+
const trimmedCardString = cardString.trim();
35+
if (!trimmedCardString) {
36+
return null;
37+
}
38+
39+
const lines = trimmedCardString.split('\n');
40+
let tags: string[] = [];
41+
let elo: number | undefined = undefined;
42+
const markdownLines = [...lines];
43+
44+
// Process the lines from bottom to top to handle metadata
45+
let metadataLines = 0;
46+
47+
// Get the configured identifiers
48+
const tagId = config.tagIdentifier || DEFAULT_PARSER_CONFIG.tagIdentifier;
49+
const eloId = config.eloIdentifier || DEFAULT_PARSER_CONFIG.eloIdentifier;
50+
51+
// Check the last few lines for metadata (tags and elo)
52+
for (let i = lines.length - 1; i >= 0 && i >= lines.length - 2; i--) {
53+
const line = lines[i].trim();
54+
55+
// Check for tags
56+
if (line.toLowerCase().startsWith(tagId!.toLowerCase())) {
57+
tags = line
58+
.substring(tagId!.length)
59+
.split(',')
60+
.map((tag) => tag.trim())
61+
.filter((tag) => tag);
62+
metadataLines++;
63+
}
64+
// Check for ELO
65+
else if (line.toLowerCase().startsWith(eloId!.toLowerCase())) {
66+
const eloValue = line.substring(eloId!.length).trim();
67+
const parsedElo = parseInt(eloValue, 10);
68+
if (!isNaN(parsedElo)) {
69+
elo = parsedElo;
70+
}
71+
metadataLines++;
72+
}
73+
}
74+
75+
// Remove metadata lines from the end of the content
76+
if (metadataLines > 0) {
77+
markdownLines.splice(markdownLines.length - metadataLines);
78+
}
79+
80+
const markdown = markdownLines.join('\n').trim();
81+
if (!markdown) {
82+
// Card must have some markdown content
83+
return null;
84+
}
85+
86+
return { markdown, tags, elo };
87+
}
88+
89+
/**
90+
* Splits a bulk text input into individual card strings
91+
*
92+
* @param bulkText - Raw string containing multiple cards
93+
* @returns Array of card strings
94+
*/
95+
export function splitCardsText(bulkText: string): string[] {
96+
return bulkText.split(CARD_DELIMITER)
97+
.map(card => card.trim())
98+
.filter(card => card); // Filter out empty strings
99+
}
100+
101+
/**
102+
* Validates if a bulk text input has valid format
103+
*
104+
* @param bulkText - Raw string containing multiple cards
105+
* @returns true if valid, false otherwise
106+
*/
107+
export function isValidBulkFormat(bulkText: string): boolean {
108+
const cardStrings = splitCardsText(bulkText);
109+
return cardStrings.length > 0 && cardStrings.some(card => !!card.trim());
110+
}

0 commit comments

Comments
 (0)