Skip to content

Commit 6a18a8a

Browse files
committed
add confirmation step to bulk inputs
1 parent 4ec3d2f commit 6a18a8a

File tree

4 files changed

+281
-102
lines changed

4 files changed

+281
-102
lines changed

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

Lines changed: 209 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<template>
22
<v-container fluid>
3-
<v-row>
3+
<v-row v-if="!parsingComplete">
44
<v-col cols="12">
55
<v-textarea
66
v-model="bulkText"
@@ -19,23 +19,91 @@ Another {{blank}}
1919
elo: 1200
2020
tags: tagC"
2121
rows="15"
22-
varient="outlined"
22+
variant="outlined"
2323
data-cy="bulk-import-textarea"
2424
></v-textarea>
2525
</v-col>
2626
</v-row>
27+
28+
<!-- Card Parsing Summary Section -->
29+
<v-row v-if="parsingComplete" class="mb-4">
30+
<v-col cols="12">
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+
<!--
49+
Future enhancement: Add a paginated/scrollable list of parsed cards here for review.
50+
-->
51+
</v-card-text>
52+
</v-card>
53+
</v-col>
54+
</v-row>
55+
2756
<v-row>
2857
<v-col cols="12">
58+
<!-- Button for initial parsing -->
2959
<v-btn
60+
v-if="!parsingComplete"
3061
color="primary"
3162
:loading="processing"
32-
:disabled="!bulkText.trim()"
33-
data-cy="bulk-import-process-btn"
34-
@click="processCards"
63+
:disabled="!bulkText.trim() || processing"
64+
data-cy="bulk-import-parse-btn"
65+
@click="handleInitialParse"
3566
>
36-
Process Cards
67+
Parse Cards
3768
<v-icon end>mdi-play-circle-outline</v-icon>
3869
</v-btn>
70+
71+
<!-- Buttons for post-parsing stage -->
72+
<template v-if="parsingComplete">
73+
<v-btn
74+
color="primary"
75+
class="mr-2"
76+
:loading="processing"
77+
:disabled="parsedCards.length === 0 || processing || importAttempted"
78+
data-cy="bulk-import-confirm-btn"
79+
@click="confirmAndImportCards"
80+
>
81+
Confirm and Import {{ parsedCards.length }} Card(s)
82+
<v-icon end>mdi-check-circle-outline</v-icon>
83+
</v-btn>
84+
<v-btn
85+
v-if="!importAttempted"
86+
variant="outlined"
87+
color="grey-darken-1"
88+
:disabled="processing"
89+
data-cy="bulk-import-edit-again-btn"
90+
@click="resetToInputStage"
91+
>
92+
<v-icon start>mdi-pencil</v-icon>
93+
Edit Again
94+
</v-btn>
95+
<v-btn
96+
v-if="importAttempted"
97+
variant="outlined"
98+
color="blue-darken-1"
99+
:disabled="processing"
100+
data-cy="bulk-import-add-another-btn"
101+
@click="startNewBulkImport"
102+
>
103+
<v-icon start>mdi-plus-circle-outline</v-icon>
104+
Add Another Bulk Import
105+
</v-btn>
106+
</template>
39107
</v-col>
40108
</v-row>
41109
<v-row v-if="results.length > 0">
@@ -45,7 +113,11 @@ tags: tagC"
45113
<v-list-item
46114
v-for="(result, index) in results"
47115
:key="index"
48-
:class="{ 'lime-lighten-5': result.status === 'success', 'red-lighten-5': result.status === 'error' }"
116+
:class="{
117+
'lime-lighten-5': result.status === 'success',
118+
'red-lighten-5': result.status === 'error',
119+
'force-dark-text': result.status === 'success' || result.status === 'error',
120+
}"
49121
>
50122
<v-list-item-title>
51123
<v-icon :color="result.status === 'success' ? 'green' : 'red'">
@@ -77,7 +149,14 @@ import { BlanksCardDataShapes } from '@vue-skuilder/courses';
77149
import { getCurrentUser } from '@vue-skuilder/common-ui';
78150
import { getDataLayer, CourseDBInterface } from '@vue-skuilder/db';
79151
import { alertUser } from '@vue-skuilder/common-ui'; // For user feedback
80-
import { ImportResult, processBulkCards, validateProcessorConfig, isValidBulkFormat } from '@/utils/bulkImport';
152+
import {
153+
ImportResult,
154+
ParsedCard,
155+
parseBulkTextToCards,
156+
importParsedCards,
157+
validateProcessorConfig,
158+
isValidBulkFormat,
159+
} from '@/utils/bulkImport';
81160
82161
export default defineComponent({
83162
name: 'BulkImportView',
@@ -90,11 +169,28 @@ export default defineComponent({
90169
data() {
91170
return {
92171
bulkText: '',
172+
parsedCards: [] as ParsedCard[],
173+
parsingComplete: false,
174+
importAttempted: false,
93175
results: [] as ImportResult[],
94-
processing: false,
176+
processing: false, // Will be used for both parsing and import stages
95177
courseDB: null as CourseDBInterface | null,
96178
};
97179
},
180+
computed: {
181+
uniqueTags(): string[] {
182+
if (!this.parsedCards || this.parsedCards.length === 0) {
183+
return [];
184+
}
185+
const allTags = this.parsedCards.reduce((acc, card) => {
186+
if (card.tags && card.tags.length > 0) {
187+
acc.push(...card.tags);
188+
}
189+
return acc;
190+
}, [] as string[]);
191+
return [...new Set(allTags)].sort();
192+
},
193+
},
98194
created() {
99195
if (this.courseCfg?.courseID) {
100196
this.courseDB = getDataLayer().getCourseDB(this.courseCfg.courseID);
@@ -116,18 +212,76 @@ export default defineComponent({
116212
}
117213
},
118214
methods: {
119-
async processCards() {
215+
resetToInputStage() {
216+
this.parsingComplete = false;
217+
this.parsedCards = [];
218+
this.importAttempted = false; // Reset import attempt flag
219+
// Optionally keep results if you want to show them even after going back
220+
// this.results = [];
221+
// this.bulkText = ''; // Optionally clear the bulk text
222+
},
223+
224+
startNewBulkImport() {
225+
this.bulkText = '';
226+
this.results = [];
227+
this.parsedCards = [];
228+
this.parsingComplete = false;
229+
this.importAttempted = false;
230+
},
231+
232+
handleInitialParse() {
120233
if (!this.courseDB) {
121234
alertUser({
122235
text: 'Database connection not available. Cannot process cards.',
123236
status: Status.error,
124237
});
125-
this.processing = false;
126238
return;
127239
}
128240
241+
// isValidBulkFormat calls alertUser internally if format is invalid.
129242
if (!isValidBulkFormat(this.bulkText)) {
243+
return;
244+
}
245+
246+
this.processing = true;
247+
this.results = []; // Clear previous import results
248+
this.parsedCards = []; // Clear previously parsed cards
249+
this.parsingComplete = false; // Reset parsing complete state
250+
251+
try {
252+
this.parsedCards = parseBulkTextToCards(this.bulkText);
253+
254+
if (this.parsedCards.length === 0) {
255+
alertUser({
256+
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.',
257+
status: Status.warning,
258+
});
259+
this.processing = false;
260+
return;
261+
}
262+
263+
// Successfully parsed, ready for review stage
264+
this.parsingComplete = true;
265+
} catch (error) {
266+
console.error('[BulkImportView] Error parsing bulk text:', error);
267+
alertUser({
268+
text: `Error parsing cards: ${error instanceof Error ? error.message : 'Unknown error'}`,
269+
status: Status.error,
270+
});
271+
} finally {
130272
this.processing = false;
273+
}
274+
},
275+
276+
async confirmAndImportCards() {
277+
if (!this.courseDB) {
278+
alertUser({ text: 'Database connection lost before import.', status: Status.error });
279+
this.processing = false; // Ensure processing is false
280+
return;
281+
}
282+
if (this.parsedCards.length === 0) {
283+
alertUser({ text: 'No parsed cards to import.', status: Status.warning });
284+
this.processing = false; // Ensure processing is false
131285
return;
132286
}
133287
@@ -142,82 +296,76 @@ export default defineComponent({
142296
}
143297
144298
this.processing = true;
145-
this.results = [];
299+
this.results = []; // Clear results from parsing stage or previous attempts
146300
147301
const currentUser = await getCurrentUser();
148302
const userName = currentUser.getUsername();
149303
150-
// Use the BlanksCardDataShapes for the data structure
151304
const dataShapeToUse: DataShape = BlanksCardDataShapes[0];
152305
153306
if (!dataShapeToUse) {
154-
this.results.push({
155-
originalText: 'N/A - Configuration Error',
156-
status: 'error',
157-
message: 'Could not find BlanksCardDataShapes. Aborting.',
158-
});
307+
alertUser({ text: 'Critical: Could not find BlanksCardDataShapes. Aborting import.', status: Status.error });
159308
this.processing = false;
160309
return;
161310
}
162311
163-
// Log the course configuration to help with debugging
164-
console.log('[BulkImportView] Processing with course config:', {
165-
courseID: this.courseCfg.courseID,
166-
dataShapes: this.courseCfg.dataShapes,
167-
questionTypes: this.courseCfg.questionTypes,
168-
dataShapeToUse: dataShapeToUse.name,
169-
});
170-
171-
// Extract course code from first dataShape in course config
172312
const configDataShape = this.courseCfg?.dataShapes?.[0];
173313
if (!configDataShape) {
174-
this.results.push({
175-
originalText: 'N/A - Configuration Error',
176-
status: 'error',
177-
message: 'No data shapes found in course configuration',
314+
alertUser({
315+
text: 'Critical: No data shapes found in course configuration. Aborting import.',
316+
status: Status.error,
178317
});
179318
this.processing = false;
180319
return;
181320
}
182321
183322
const codeCourse = NameSpacer.getDataShapeDescriptor(configDataShape.name).course;
184-
console.log(`[BulkImportView] Using codeCourse: ${codeCourse} for note addition`);
185323
186-
// Prepare processor configuration
187-
const config = {
324+
const processorConfig = {
188325
dataShape: dataShapeToUse,
189326
courseCode: codeCourse,
190327
userName: userName,
191328
};
192329
193-
// Validate processor configuration
194-
const validation = validateProcessorConfig(config);
330+
const validation = validateProcessorConfig(processorConfig);
195331
if (!validation.isValid) {
196-
this.results.push({
197-
originalText: 'N/A - Configuration Error',
198-
status: 'error',
199-
message: validation.errorMessage || 'Invalid processor configuration',
332+
alertUser({
333+
text: validation.errorMessage || 'Invalid processor configuration for import.',
334+
status: Status.error,
200335
});
201336
this.processing = false;
202337
return;
203338
}
204339
205-
// Process the cards
340+
console.log('[BulkImportView] Starting import of parsed cards:', {
341+
courseID: this.courseCfg.courseID,
342+
dataShapeToUse: dataShapeToUse.name,
343+
courseCode: codeCourse,
344+
numberOfCards: this.parsedCards.length,
345+
});
346+
206347
try {
207-
this.results = await processBulkCards(this.bulkText, this.courseDB, config);
348+
this.results = await importParsedCards(this.parsedCards, this.courseDB, processorConfig);
208349
} catch (error) {
209-
console.error('[BulkImportView] Error processing cards:', error);
350+
console.error('[BulkImportView] Error importing parsed cards:', error);
210351
this.results.push({
211-
originalText: this.bulkText,
352+
originalText: 'Bulk Operation Error',
212353
status: 'error',
213-
message: `Error processing cards: ${error instanceof Error ? error.message : 'Unknown error'}`,
354+
message: `Critical error during import: ${error instanceof Error ? error.message : 'Unknown error'}`,
214355
});
356+
} finally {
357+
this.processing = false;
358+
this.importAttempted = true; // Mark that an import attempt has been made
215359
}
216360
217-
this.processing = false;
218-
if (!this.results.some((r) => r.status === 'error')) {
219-
// Potentially clear bulkText if all successful, or offer a button to do so
220-
// this.bulkText = '';
361+
if (this.results.every((r) => r.status === 'success') && this.results.length > 0) {
362+
// All successful, optionally reset
363+
// this.bulkText = ''; // Clear input text
364+
// this.parsingComplete = false; // Go back to input stage
365+
// this.parsedCards = [];
366+
alertUser({ text: `${this.results.length} card(s) imported successfully!`, status: Status.success });
367+
} else if (this.results.some((r) => r.status === 'error')) {
368+
alertUser({ text: 'Some cards failed to import. Please review the results below.', status: Status.warning });
221369
}
222370
},
223371
},
@@ -239,4 +387,16 @@ pre {
239387
border-radius: 4px;
240388
margin-top: 5px;
241389
}
390+
.force-dark-text {
391+
color: rgba(0, 0, 0, 0.87) !important;
392+
}
393+
/* Ensure child elements also get dark text if not overridden */
394+
.force-dark-text .v-list-item-subtitle,
395+
.force-dark-text .v-list-item-title,
396+
.force-dark-text div, /* Ensure divs within the list item also get dark text */
397+
.force-dark-text summary {
398+
/* Ensure summary elements for <details> also get dark text */
399+
color: rgba(0, 0, 0, 0.87) !important;
400+
}
401+
/* Icons are handled by their :color prop, so no specific override needed here unless that changes. */
242402
</style>

0 commit comments

Comments
 (0)