Release v0.1.4
This commit is contained in:
@@ -1,9 +1,15 @@
|
||||
import {
|
||||
buildImportTableFromRows,
|
||||
applyRecipientMappingProfile,
|
||||
buildImportTable,
|
||||
buildRecipientImportPreview,
|
||||
createRecipientImportProvenance,
|
||||
createRecipientMappingProfile,
|
||||
defaultColumnMappings,
|
||||
matchRecipientMappingProfiles,
|
||||
materializeRecipientImport,
|
||||
parseDelimitedText,
|
||||
xlsxSheetsFromArrayBuffer,
|
||||
type RecipientColumnMapping
|
||||
} from "../src/features/campaigns/utils/bulkImport";
|
||||
|
||||
@@ -19,6 +25,15 @@ function mappingFor(mappings: RecipientColumnMapping[], columnIndex: number): Re
|
||||
return mappings.find((mapping) => mapping.columnIndex === columnIndex) ?? { columnIndex, kind: "ignore" };
|
||||
}
|
||||
|
||||
function base64ToArrayBuffer(value: string): ArrayBuffer {
|
||||
const binary = atob(value);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let index = 0; index < binary.length; index += 1) {
|
||||
bytes[index] = binary.charCodeAt(index);
|
||||
}
|
||||
return bytes.buffer;
|
||||
}
|
||||
|
||||
const parsed = parseDelimitedText('email;name;field.note\n"alice@example.org";"Alice; A";"hello"', ";");
|
||||
assert(parsed[1][1] === "Alice; A", "quoted delimiters are preserved");
|
||||
|
||||
@@ -42,6 +57,44 @@ assert(table.headerRows.length === 1 && table.dataRows.length === 3, "header and
|
||||
assert(mappingFor(mappings, 0).kind === "to", "email columns map to To");
|
||||
assert(mappingFor(mappings, 2).kind === "field" && mappingFor(mappings, 2).fieldName === "customer_id", "existing fields are detected");
|
||||
assert(mappingFor(mappings, 3).kind === "new_field" && mappingFor(mappings, 3).newFieldName === "contract_no", "explicit new field columns are detected");
|
||||
|
||||
const profile = createRecipientMappingProfile({
|
||||
id: "profile-1",
|
||||
name: "ERP export",
|
||||
table,
|
||||
mappings,
|
||||
parseOptions: { headerRows: 1, quoted: true },
|
||||
valueSeparators: ",;|",
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z"
|
||||
});
|
||||
const exactProfileMatches = matchRecipientMappingProfiles(table, [profile]);
|
||||
assert(exactProfileMatches[0]?.mode === "ordered" && exactProfileMatches[0].score === 1, "ordered header fingerprints match exactly");
|
||||
|
||||
const reorderedTable = buildImportTable(
|
||||
[
|
||||
"name;email;active;attachment_pattern;field.contract_no;customer_id",
|
||||
"Alice;alice@example.org;yes;${customer_id}.pdf;CN-1;C-1"
|
||||
].join("\n"),
|
||||
{ delimiter: "auto", headerRows: 1, quoted: true }
|
||||
);
|
||||
const reorderedMatches = matchRecipientMappingProfiles(reorderedTable, [profile]);
|
||||
assert(reorderedMatches[0]?.mode === "unordered", "unordered header fingerprints match reordered exports");
|
||||
const reorderedMappings = applyRecipientMappingProfile(reorderedTable, profile, existingFields);
|
||||
assert(mappingFor(reorderedMappings, 0).kind === "name", "reordered profiles apply mappings by header");
|
||||
assert(mappingFor(reorderedMappings, 1).kind === "to", "email mapping follows the reordered header");
|
||||
assert(mappingFor(reorderedMappings, 4).kind === "new_field" && mappingFor(reorderedMappings, 4).newFieldName === "contract_no", "new field mappings follow reordered headers");
|
||||
|
||||
const similarTable = buildImportTable(
|
||||
[
|
||||
"email;name;customer_id;attachment_pattern",
|
||||
"alice@example.org;Alice;C-1;${customer_id}.pdf"
|
||||
].join("\n"),
|
||||
{ delimiter: "auto", headerRows: 1, quoted: true }
|
||||
);
|
||||
const similarMatches = matchRecipientMappingProfiles(similarTable, [profile]);
|
||||
assert(similarMatches[0]?.mode === "similar", "similar profiles are suggested when headers changed");
|
||||
|
||||
assert(preview.validCount === 1, "one valid row is detected");
|
||||
assert(preview.invalidCount === 2, "invalid rows are detected");
|
||||
assert(preview.patternCount === 3, "all pattern cells are counted before invalid rows are skipped");
|
||||
@@ -76,3 +129,33 @@ assert(importedEntry.email === "alice@example.org", "entry email is materialized
|
||||
assert(attachments.length === 2, "valid row patterns become attachment rules");
|
||||
assert(attachments[0].base_path_id === "bp-1" && attachments[0].base_dir === "invoices", "attachment rules use the selected source");
|
||||
assert(attachments[0].file_filter === "${customer_id}.pdf", "field placeholders remain in imported patterns");
|
||||
|
||||
void runXlsxImportAssertions();
|
||||
|
||||
async function runXlsxImportAssertions(): Promise<void> {
|
||||
const workbookBuffer = base64ToArrayBuffer("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");
|
||||
const workbookSheets = await xlsxSheetsFromArrayBuffer(workbookBuffer);
|
||||
assert(workbookSheets[0]?.name === "Recipients", "xlsx sheets are listed");
|
||||
const xlsxTable = buildImportTableFromRows(workbookSheets[0].rows, { headerRows: 1 });
|
||||
const xlsxMappings = defaultColumnMappings(xlsxTable.headers, []);
|
||||
const xlsxPreview = buildRecipientImportPreview(xlsxTable, xlsxMappings, { existingFields: [], valueSeparators: ",;|" });
|
||||
assert(xlsxPreview.validCount === 1, "xlsx rows become import preview rows");
|
||||
assert(mappingFor(xlsxMappings, 2).kind === "new_field" && mappingFor(xlsxMappings, 2).newFieldName === "department", "xlsx headers use the same mapping guesser");
|
||||
|
||||
const provenance = createRecipientImportProvenance({
|
||||
preview: xlsxPreview,
|
||||
mappings: xlsxMappings,
|
||||
mode: "replace",
|
||||
sourceType: "xlsx",
|
||||
filename: "recipients.xlsx",
|
||||
sheetName: "Recipients",
|
||||
headerRows: 1,
|
||||
valueSeparators: ",;|"
|
||||
});
|
||||
const importedWithProvenance = materializeRecipientImport(draft, xlsxPreview, { mode: "replace", provenance });
|
||||
const provenanceEntries = asRecord(importedWithProvenance.entries);
|
||||
const imports = provenanceEntries.imports as Record<string, unknown>[];
|
||||
assert(imports.length === 1, "recipient import provenance is stored with entries");
|
||||
assert(imports[0].source_type === "xlsx" && imports[0].sheet_name === "Recipients", "xlsx provenance records source details");
|
||||
assert((imports[0].mapping as Record<string, unknown>[]).length === xlsxTable.headers.length, "provenance stores column mapping");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user