mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-08-18 16:37:18 +02:00
add frontend check for csv parse warning and errors
Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
@@ -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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user