Import CSV or XLSX into any collection: upload (encoding and delimiter sniffing, sheet, header row, the file's number/date format) → column mapping (locale-folded auto-mapping and saved templates) → review with inline fixes → duplicate strategy, dry run and a chunked, cancellable import → report with a failed-rows download. papaparse and read-excel-file load only when a file is read.
Installation
The whole kit as one package — get updates by bumping the version. Add the styles to globals.css: @import "@vamos-ui/react/styles.css";
Usage
import { ImportWizard, type ImportField } from "@vamos-ui/react/business";
const fields: ImportField[] = [
{ id: "name", label: "Full name", aliases: ["Ad Soyad"], required: true, type: "text" },
{ id: "email", label: "Email", aliases: ["E-posta", "Mail"], required: true, type: "email" },
{ id: "value", label: "Deal value", type: "money" },
];
<ImportWizard
fields={fields}
dedupeKeys={["email"]}
onImport={(rows, { signal, strategy, dryRun, onProgress }) => api.contacts.bulk(rows, { signal, strategy, dryRun })}
/>The drop zone is a real file button. Review counts are aria-live, error cells are labelled inputs ("Email, row 2") with their message linked, progress is a progressbar and the report has a focused heading, summary text and a table.
Examples
Locale-pack validators
HR bulk import: TCKN and IBAN checked with the tr pack, "01.12.2026" and "96.000,00" parsed in the file's Turkish format. Fix bad cells inline; F8 jumps to the next error.
Saved mapping templates
Headers that don't auto-map are mapped once and saved; templates.list() offers them next time.
Windows-1254 semicolon CSV
Download the file and drop it in: the encoding and ; delimiter are sniffed, so Turkish letters come out right. If a file still looks garbled, a banner offers Windows-1254.
API
ImportWizard
| Prop | Type | Default |
|---|---|---|
fields{ id, label, aliases?, required?, type, options?, validate?(v, row), normalize?(v) }. type: text · number · money · date · boolean · enum · email · phone · id · iban. | ImportField[] | — |
onImportCalled per chunk with typed values. ctx = { signal, onProgress, strategy, dryRun, offset }; failed rows index into the chunk. | (rows, ctx) => Promise<{ ok; failed: { row; error }[] }> | — |
dedupeKeysFields that identify a record: duplicates in the file become warnings and the duplicate strategy (skip / update / create) is offered. | string[] | — |
chunkSizeRows per onImport call; cancel stops between chunks. | number | 200 |
maxRows / maxSizeLimits checked on upload. | number | 50,000 / 20 MB |
templatesSaved column mappings. | { list(): Promise<MappingTemplate[]>; save(t): Promise<void> } | — |
sampleFile"Download a sample file" link. | { name; href } | — |
fileLocalesChoices for the file's number/date format (defaults to the UI locale). | { value; label }[] | — |
previewLimitRows rendered in the review table. | number | 200 |
onDoneShows a Done button on the report. | (report: ImportReport) => void | — |
labelsBuilt-in strings (importWizard namespace). | Partial<ImportWizardLabels> | — |
ImportMapping / ImportReview
| Prop | Type | Default |
|---|---|---|
ImportMappingThe mapping list on its own: one labelled Select per column. | { headers, sample?, fields, mapping, onMappingChange } | — |
ImportReviewThe validation preview on its own. | { results, fields, onlyErrors, onOnlyErrorsChange, errorCount, warningCount, onFix? } | — |
core (worker-safe)
| Prop | Type | Default |
|---|---|---|
sniffCsv(bytes)UTF-8 vs Windows-1254 / 1252, and , ; or tab. | { encoding, delimiter, hasBom } | — |
autoMap(headers, fields, locale)Folded exact, then contains matching (tr "E-posta" ≈ "eposta"). | Record<column, fieldId | null> | — |
parseCell(v, field, locale)"1.234,50", "10.11.2026", Excel serial dates, yes/no/evet/hayır. | { value, error? } | — |
validateImport(rows, mapping, fields, { locale, dedupeKeys })Typed values, errors and warnings per field. | ImportRowResult[] | — |