Importing

Rows from a spreadsheet or CSV mapped to fields, matched to records, reviewed line by line, and applied as one write.

An import is a write like any other: its rows are mapped to your fields, matched to the records they update, parsed the way a paste is, judged by your rules and capabilities, shown for review, and committed as one change. You see what each line of the file would do before anything changes, and afterwards what each line did.

Reading the file

The readers are an opt-in entry: a grid that never imports never ships them.

import { readCsv, readXlsx } from '@witbybit/wit-grid-react/import';

async function read(file: File, text: string) {
	const workbook = await readXlsx(file); // a File, a Blob or its bytes: read in the browser, nothing uploaded
	const sheet = workbook.sheets[0]; // { name, rows }: the header row first
	const csv = readCsv(text, { name: 'AP export' });
	return { sheet, csv };
}

readXlsx reads values: text, numbers, booleans and dates (told apart by the cell's number format; formulas give their cached values). Nothing in a workbook runs: macros, external links and embedded objects are never read. What it cannot read it lists in workbook.problems by sheet and cell, with a kind (errorValue: a #N/A read as empty; notWorksheet: a chart sheet left out). Any other reader works too: hand the grid rows of string, number, boolean, Date or null.

The wizard

For people importing a file themselves, openImportWizard does the whole journey over the grid, in the grid's theme and words:

import { openImportWizard } from '@witbybit/wit-grid-react/import';
import type { GridApi } from '@witbybit/wit-grid-react';

type Invoice = { id: string; vendor: string; amount: number; status: string };
declare const api: GridApi<Invoice>;
declare const file: File;

const result = await openImportWizard(api, {
	source: file, // a .csv is read as CSV, anything else as .xlsx; or a workbook or sheet already read
	key: 'id',
	create: (values) => ({ id: String(values.id), vendor: String(values.vendor ?? ''), amount: Number(values.amount ?? 0), status: 'received' }),
	policy: 'bestEffort',
});
// null when closed before importing; else the import's result, line by line
  1. The sheet (when the workbook has several) and the header row, with its first rows shown.
  2. The columns: each column in the file, an example of its values, and the field it fills (preset from the headers; any column can be skipped), and the field records are matched by (preset from a column whose role is key; otherwise the person chooses: matching is never guessed).
  3. The check: what the file would do (new, to update, unchanged, with problems) and every problem line; a value a column cannot read is corrected in place and checked again.
  4. The import runs with { review: true }: your review presenter (or the workspace's dialog) shows the prepared write, then the wizard says what each line did.

It calls only planImport and import, so anything it does your own screens can do. Its words are the importWizard messages area: messages: { importWizard: { title: 'Datensätze importieren' } }.

Planning: what each line would do

import type { GridApi, GridImportSheet } from '@witbybit/wit-grid-react';

type Invoice = { id: string; vendor: string; amount: number; status: string };
declare const api: GridApi<Invoice>;
declare const sheet: GridImportSheet;

const plan = await api.data.planImport({
	sheet,
	key: 'id', // the field that identifies a record
	create: (values) => ({ id: String(values.id), vendor: String(values.vendor ?? ''), amount: Number(values.amount ?? 0), status: 'received' }),
});
plan.counts; // { add, update, unchanged, invalid }
plan.rows; // per line: { line, outcome, rowId?, problems: [{ field?, column?, reason }] }
  • Mapping: a header matching a column's header or field maps to it; mapping: { 'Invoice no.': 'id', Notes: null } says otherwise (null skips a column). Fields the file does not map are never touched.
  • Matching: key is a field matched against the records the grid holds. A key in the file twice, a key two records share, or a missing key makes the line invalid, saying so. Remote data (the grid holds only some records) needs your own lookup, key: { resolve: (values) => rowIdOrNull }; the grid never guesses that an unloaded record is new.
  • New records: create(values) builds one from a line's values. Without it, a line that matches no record is refused, never added.
  • Values go through each column's parse, as a paste does; a value a column cannot read is a problem on that line, with its column letter. overrides: { 5: { amount: '$1,200' } } corrects line 5's amount (text is parsed as a typed value is). A blank cell leaves the field as it is (blanks: 'clear' empties it instead).

Planning changes nothing.

Applying it

import type { GridApi, GridImportSheet } from '@witbybit/wit-grid-react';

type Invoice = { id: string; amount: number };
declare const api: GridApi<Invoice>;
declare const sheet: GridImportSheet;

const result = await api.data.import({ sheet, key: 'id', policy: 'bestEffort' }, { review: true });
result.status; // applied, refused, cancelled, nothing or unavailable
result.rows; // per line: result 'added' | 'updated' | 'unchanged' | 'refused' | 'notAttempted', and why

It runs as the operation a writing command is: prepared as the user, so your rules judge each record (a new one as a whole) and canCreate and canEdit apply; shown for review with { review: true } through the grid's review presenter (or the workspace's dialog); committed as one change and one undo entry, named import (or the request's action). If a record or the rules change while the review is open, it is planned again from the same file and shown again.

  • atomic (the default): any line that cannot be imported stops it all, and nothing is imported.
  • bestEffort: what can apply does; a line the rules refuse is reported with their reason.
  • Applied here is not accepted by your server: with data.save, the import is one queued change (result.change), followed to the server's answer like any other.
  • At most 10,000 lines are taken (limit); the rest are counted as not attempted.
Edit on GitHub

Last updated on

On this page