Skip to content

Commit 59242a3

Browse files
committed
feat: enhance CSV import error handling and validation
1 parent ec2cec0 commit 59242a3

File tree

1 file changed

+82
-38
lines changed

1 file changed

+82
-38
lines changed

core/ui/src/components/domains/ImportUsersModal.vue

Lines changed: 82 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,8 @@
4747
<div
4848
v-if="error.uploadCsvFile"
4949
class="validation-failed-invalid-message"
50-
>
51-
{{ $t(error.uploadCsvFile) }}
52-
</div>
50+
v-html="$t(error.uploadCsvFile)"
51+
></div>
5352
<label class="bx--label">
5453
{{ $t("import_users.manage_existing_users") }}
5554
<cv-interactive-tooltip
@@ -294,50 +293,92 @@ export default {
294293
reader.onload = (e) => {
295294
try {
296295
const csvContent = e.target.result;
296+
297+
// First parse with auto-detection to determine delimiter
298+
const detectionResults = Papa.parse(csvContent, {
299+
preview: 1,
300+
skipEmptyLines: true,
301+
});
302+
303+
// Check if first row contains valid headers
304+
const firstRow = detectionResults.data[0] || [];
305+
const trimmedFirstRow = firstRow.map((cell) =>
306+
typeof cell === "string" ? cell.trim() : cell
307+
);
308+
309+
// Detect if header row is present by checking if first row matches expected columns
310+
const hasValidHeaders = this.tableColumns.every((col) =>
311+
trimmedFirstRow.includes(col)
312+
);
313+
314+
if (!hasValidHeaders) {
315+
// Show which columns are expected with line breaks
316+
const expectedCols = this.tableColumns.join(", ");
317+
const foundCols = trimmedFirstRow.length > 0
318+
? trimmedFirstRow.join(", ")
319+
: this.$t("import_users.no_header_found");
320+
321+
this.error.uploadCsvFile =
322+
this.$t("import_users.csv_missing_header") +
323+
"<br>" +
324+
this.$t("import_users.expected_columns", { columns: expectedCols }) +
325+
"<br>" +
326+
this.$t("import_users.found_columns", { columns: foundCols });
327+
this.loading.getPreviewData = false;
328+
return;
329+
}
330+
331+
// Parse with detected delimiter and header support
297332
const results = Papa.parse(csvContent, {
298-
header: false, // Important: no header, we process columns by index
333+
header: true, // Use first row as headers
299334
skipEmptyLines: true,
335+
delimiter: detectionResults.meta.delimiter, // Use auto-detected delimiter
336+
transformHeader: (header) => header.trim(), // Trim header names
300337
});
301338
302339
// Check if Papa parse encountered errors
303340
if (results.errors && results.errors.length > 0) {
304-
this.error.uploadCsvFile = "import_users.invalid_csv_format";
341+
// Build detailed error message showing line-by-line issues
342+
const errorDetails = results.errors
343+
.slice(0, 5) // Show first 5 errors max
344+
.map(err => {
345+
if (err.type === 'FieldMismatch') {
346+
return this.$t('import_users.csv_column_mismatch', {
347+
line: err.row + 2, // +2 because row is 0-indexed and we have header
348+
expected: results.meta.fields.length,
349+
found: err.row < results.data.length ? Object.keys(results.data[err.row]).length : '?'
350+
});
351+
}
352+
return `${this.$t('import_users.line')} ${err.row + 2}: ${err.message}`;
353+
})
354+
.join('<br>');
355+
356+
const moreErrors = results.errors.length > 5
357+
? '<br>' + this.$t('import_users.and_more_errors', { count: results.errors.length - 5 })
358+
: '';
359+
360+
this.error.uploadCsvFile =
361+
this.$t('import_users.csv_parse_errors') + '<br>' + errorDetails + moreErrors;
305362
this.loading.getPreviewData = false;
306363
return;
307364
}
308365
309-
// Check if first row is the header and remove it
310-
const headerString =
311-
"user,display_name,password,mail,groups,locked,must_change_password,no_password_expiration";
312-
if (
313-
results.data.length > 0 &&
314-
results.data[0].join(",") === headerString
315-
) {
316-
results.data = results.data.slice(1);
317-
}
366+
// Validate that all required columns are present (double-check after parsing)
367+
const requiredColumns = this.tableColumns;
368+
const headers = results.meta.fields || [];
318369
319-
// Define expected column
320-
let expectedColumns = this.tableColumns.length;
321-
// Validate column count in all rows
322-
for (let i = 0; i < results.data.length; i++) {
323-
if (results.data[i].length !== expectedColumns) {
324-
this.error.uploadCsvFile =
325-
"import_users.invalid_csv_format_not_expected_columns";
326-
this.loading.getPreviewData = false;
327-
return;
328-
}
329-
}
370+
const missingColumns = requiredColumns.filter(
371+
(col) => !headers.includes(col)
372+
);
330373
331-
let COLUMN_MAPPING = {
332-
0: "user",
333-
1: "display_name",
334-
2: "password",
335-
3: "mail",
336-
4: "groups",
337-
5: "locked",
338-
6: "must_change_password",
339-
7: "no_password_expiration",
340-
};
374+
if (missingColumns.length > 0) {
375+
this.error.uploadCsvFile = this.$t(
376+
"import_users.csv_missing_columns",
377+
{ columns: missingColumns.join(", ") }
378+
);
379+
this.loading.getPreviewData = false;
380+
return;
381+
}
341382
342383
// Define which fields should be booleans
343384
const booleanFields = [
@@ -346,11 +387,13 @@ export default {
346387
"no_password_expiration",
347388
];
348389
349-
// Transform rows into objects with correct keys
390+
// Transform rows - Papa.parse already created objects with headers as keys
350391
this.importData = results.data.map((row) => {
351392
const obj = {};
352-
Object.entries(COLUMN_MAPPING).forEach(([index, key]) => {
353-
let value = row[index];
393+
394+
// Process each expected column
395+
this.tableColumns.forEach((key) => {
396+
let value = row[key];
354397
355398
// Trim whitespace from string values
356399
if (typeof value === "string") {
@@ -378,6 +421,7 @@ export default {
378421
}
379422
obj[key] = value;
380423
});
424+
381425
return obj;
382426
});
383427

0 commit comments

Comments
 (0)