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