add frontend check for csv parse warning and errors

Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
Ronni Skansing
2025-12-03 22:14:48 +01:00
parent a63b9a014b
commit cbaca17756
3 changed files with 84 additions and 12 deletions
+36 -5
View File
@@ -3,17 +3,26 @@ import papaparse from 'papaparse';
/**
* Parse CSV file to recipients
* @param {File} file - CSV file
* @returns {Promise<Array<*>>}
* @returns {Promise<{recipients: Array<*>, skipped: Array<{line: number, reason: string, row: object}>}>}
**/
export const parseCSVToRecipients = async (file) => {
const p = new Promise((resolve, reject) => {
const recipients = {};
const skipped = [];
papaparse.parse(file, {
header: true,
skipEmptyLines: true,
complete: (results) => {
if (results.errors) {
if (results.errors && results.errors.length > 0) {
console.info('CSV import errors', results.errors);
// track parsing errors
results.errors.forEach((error) => {
skipped.push({
line: error.row + 2, // +1 for header, +1 for 0-index
reason: `parse error: ${error.message}`,
row: error.row
});
});
}
if (!results.data) {
reject('No data found in CSV file');
@@ -26,9 +35,23 @@ export const parseCSVToRecipients = async (file) => {
fieldsMap[field.toLowerCase()] = field;
}
results.data.forEach((row) => {
results.data.forEach((row, index) => {
const email = row[fieldsMap['email']];
if (!email) {
skipped.push({
line: index + 2, // +1 for header, +1 for 0-index
reason: 'missing email',
row: row
});
return;
}
// check if email already exists in this import (duplicate within file)
if (recipients[email]) {
skipped.push({
line: index + 2,
reason: `duplicate email in file (first occurrence at line ${recipients[email]._line})`,
row: row
});
return;
}
recipients[email] = {
@@ -41,10 +64,18 @@ export const parseCSVToRecipients = async (file) => {
department: row[fieldsMap['department']] ?? null,
city: row[fieldsMap['city']] ?? null,
country: row[fieldsMap['country']] ?? null,
misc: row[fieldsMap['misc']] ?? null
misc: row[fieldsMap['misc']] ?? null,
_line: index + 2 // track line number for duplicate detection
};
});
resolve(Object.values(recipients));
// remove internal _line property before returning
const recipientsList = Object.values(recipients).map((r) => {
const { _line, ...recipient } = r;
return recipient;
});
resolve({ recipients: recipientsList, skipped });
}
});
});
+26 -5
View File
@@ -62,6 +62,7 @@
recipients: [],
ignoreOverwriteEmptyFields: true
};
let csvSkippedRows = [];
const tableImportParams = newTableParams({ sortBy: 'email' });
let selectedRecipientsImportPaginatedChunk = [];
let isImportModalVisible = false;
@@ -209,12 +210,12 @@
importModalError = res.error;
return;
}
addToast('Recipients imported to group', 'Success');
addToast('Recipients imported', 'Success');
closeImportModal();
refreshRecipients();
} catch (err) {
addToast('Failed to import recipients to group', 'Error');
console.error('failed to import recipients to group', err);
addToast('Failed to import recipients', 'Error');
console.error('failed to import recipients', err);
} finally {
isSubmitting = false;
}
@@ -228,9 +229,16 @@
showIsLoading();
for (let i = 0; i < files.length; i++) {
const file = files[i];
const recipientsForImport = await parseCSVToRecipients(file);
const result = await parseCSVToRecipients(file);
// track skipped rows
if (result.skipped && result.skipped.length > 0) {
csvSkippedRows = csvSkippedRows.concat(result.skipped);
console.info(`CSV import: ${result.skipped.length} rows skipped`, result.skipped);
}
importFormValues.recipients = importFormValues.recipients.concat(
recipientsForImport.filter(
result.recipients.filter(
(recipient) =>
!importFormValues.recipients.some(
(existingRecipient) => existingRecipient.email === recipient.email
@@ -238,6 +246,17 @@
)
);
refreshImportsPaginated();
// show info about skipped rows
if (result.skipped && result.skipped.length > 0) {
const skippedMsg = result.skipped
.slice(0, 3)
.map((s) => `Line ${s.line}: ${s.reason}`)
.join('\n');
const remaining =
result.skipped.length > 3 ? `\n... and ${result.skipped.length - 3} more` : '';
importModalError = `CSV rows skipped:\n${skippedMsg}${remaining}\n\nReview the data before importing.`;
}
}
} catch (e) {
importModalError = e;
@@ -268,6 +287,8 @@
};
const openImportModal = () => {
csvSkippedRows = [];
importModalError = '';
isImportModalVisible = true;
};
@@ -61,6 +61,7 @@
recipients: [],
ignoreOverwriteEmptyFields: true
};
let csvSkippedRows = [];
const tableImportParams = newTableParams({ sortBy: 'email' });
let selectedRecipientsImportPaginatedChunk = [];
let isImportModalVisible = false;
@@ -281,9 +282,15 @@
try {
for (let i = 0; i < files.length; i++) {
const file = files[i];
const recipientsForImport = await parseCSVToRecipients(file);
const result = await parseCSVToRecipients(file);
// track skipped rows
if (result.skipped && result.skipped.length > 0) {
console.info(`CSV import: ${result.skipped.length} rows skipped`, result.skipped);
}
importFormValues.recipients = importFormValues.recipients.concat(
recipientsForImport.filter(
result.recipients.filter(
(recipient) =>
!importFormValues.recipients.some(
(existingRecipient) => existingRecipient.email === recipient.email
@@ -291,6 +298,17 @@
)
);
refreshImportsPaginated();
// show info about skipped rows
if (result.skipped && result.skipped.length > 0) {
const skippedMsg = result.skipped
.slice(0, 3)
.map((s) => `Line ${s.line}: ${s.reason}`)
.join('\n');
const remaining =
result.skipped.length > 3 ? `\n... and ${result.skipped.length - 3} more` : '';
importError = `CSV rows skipped:\n${skippedMsg}${remaining}\n\nReview the data below before importing.`;
}
}
} catch (e) {
importError = e;
@@ -333,6 +351,8 @@
};
const openImportModal = () => {
csvSkippedRows = [];
importError = '';
isImportModalVisible = true;
};