@expresscsv/react 0.1.21 → 0.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -245
- package/dist/index.d.cts +82 -84
- package/dist/index.d.mts +82 -84
- package/dist/index.d.ts +82 -84
- package/dist/index.js +2 -2
- package/dist/index.mjs +2 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@expresscsv/react)
|
|
4
4
|

|
|
5
5
|
|
|
6
|
-
React hook for embedding the [ExpressCSV](https://expresscsv.com) CSV
|
|
6
|
+
React hook for embedding the [ExpressCSV](https://expresscsv.com) CSV importer. Wraps [`@expresscsv/sdk`](https://www.npmjs.com/package/@expresscsv/sdk) with automatic lifecycle management and reactive state.
|
|
7
7
|
|
|
8
8
|
If you want to define schemas in shared or backend code without any frontend dependencies, use [`@expresscsv/schemas`](https://www.npmjs.com/package/@expresscsv/schemas) for the schema definition itself, then pass that schema into `@expresscsv/react`.
|
|
9
9
|
|
|
@@ -36,7 +36,7 @@ const schema = x.row({
|
|
|
36
36
|
function App() {
|
|
37
37
|
const { open, isOpen } = useExpressCSV({
|
|
38
38
|
schema,
|
|
39
|
-
|
|
39
|
+
getSessionToken: async () => fetchSessionToken(),
|
|
40
40
|
importIdentifier: "user-import",
|
|
41
41
|
title: "Import Users",
|
|
42
42
|
});
|
|
@@ -48,17 +48,19 @@ function App() {
|
|
|
48
48
|
console.log(
|
|
49
49
|
`Chunk ${chunk.currentChunkIndex + 1}/${chunk.totalChunks}`
|
|
50
50
|
);
|
|
51
|
+
console.log("Session:", chunk.sessionId);
|
|
52
|
+
console.log("Idempotency key:", chunk.chunkIdempotencyKey);
|
|
51
53
|
console.log("Records:", chunk.records);
|
|
52
54
|
next();
|
|
53
55
|
},
|
|
54
|
-
onComplete: () => {
|
|
55
|
-
console.log("All chunks processed");
|
|
56
|
+
onComplete: ({ sessionId }) => {
|
|
57
|
+
console.log("All chunks processed for", sessionId);
|
|
56
58
|
},
|
|
57
|
-
onCancel: () => {
|
|
58
|
-
console.log("User cancelled");
|
|
59
|
+
onCancel: ({ sessionId }) => {
|
|
60
|
+
console.log("User cancelled", sessionId);
|
|
59
61
|
},
|
|
60
|
-
onError: (error) => {
|
|
61
|
-
console.error("Import error
|
|
62
|
+
onError: (error, { sessionId }) => {
|
|
63
|
+
console.error("Import error for", sessionId, error);
|
|
62
64
|
},
|
|
63
65
|
});
|
|
64
66
|
};
|
|
@@ -71,20 +73,20 @@ function App() {
|
|
|
71
73
|
}
|
|
72
74
|
```
|
|
73
75
|
|
|
74
|
-
|
|
76
|
+
Have your backend keep the ExpressCSV secret key, call the ExpressCSV session-creation endpoint, and return a short-lived importer session token to the browser via `getSessionToken`.
|
|
75
77
|
|
|
76
78
|
## Preloading
|
|
77
79
|
|
|
78
|
-
By default the
|
|
80
|
+
By default the importer preloads in a hidden iframe so it appears instantly when `open()` is called:
|
|
79
81
|
|
|
80
82
|
```tsx
|
|
81
83
|
const { open } = useExpressCSV({
|
|
82
84
|
schema,
|
|
83
|
-
|
|
85
|
+
getSessionToken: async () => fetchSessionToken(),
|
|
84
86
|
importIdentifier: "user-import",
|
|
85
87
|
});
|
|
86
88
|
|
|
87
|
-
//
|
|
89
|
+
// Importer displays instantly
|
|
88
90
|
const handleImport = () => {
|
|
89
91
|
open({
|
|
90
92
|
onData: (chunk, next) => {
|
|
@@ -100,7 +102,7 @@ To disable preloading (there will be a brief loading screen instead):
|
|
|
100
102
|
```tsx
|
|
101
103
|
const { open } = useExpressCSV({
|
|
102
104
|
schema,
|
|
103
|
-
|
|
105
|
+
getSessionToken: async () => fetchSessionToken(),
|
|
104
106
|
importIdentifier: "user-import",
|
|
105
107
|
preload: false,
|
|
106
108
|
});
|
|
@@ -124,7 +126,7 @@ const candidateSchema = x.row({
|
|
|
124
126
|
|
|
125
127
|
const { open } = useExpressCSV({
|
|
126
128
|
schema: candidateSchema,
|
|
127
|
-
|
|
129
|
+
getSessionToken: async () => fetchSessionToken(),
|
|
128
130
|
importIdentifier: "candidate-import",
|
|
129
131
|
templateDownload: {
|
|
130
132
|
source: "generate",
|
|
@@ -142,7 +144,7 @@ const { open } = useExpressCSV({
|
|
|
142
144
|
|
|
143
145
|
## Theming and Styling
|
|
144
146
|
|
|
145
|
-
Customize the
|
|
147
|
+
Customize the importer's appearance with the `theme`, `colorMode`, `customCSS`, and `fonts` options.
|
|
146
148
|
|
|
147
149
|
### Theme
|
|
148
150
|
|
|
@@ -181,7 +183,7 @@ const dualTheme: ECSVTheme = {
|
|
|
181
183
|
function App() {
|
|
182
184
|
const { open } = useExpressCSV({
|
|
183
185
|
schema,
|
|
184
|
-
|
|
186
|
+
getSessionToken: async () => fetchSessionToken(),
|
|
185
187
|
importIdentifier: "user-import",
|
|
186
188
|
theme,
|
|
187
189
|
});
|
|
@@ -227,7 +229,7 @@ Control light/dark mode with `colorMode`:
|
|
|
227
229
|
```tsx
|
|
228
230
|
const { open } = useExpressCSV({
|
|
229
231
|
schema,
|
|
230
|
-
|
|
232
|
+
getSessionToken: async () => fetchSessionToken(),
|
|
231
233
|
importIdentifier: "user-import",
|
|
232
234
|
colorMode: "system", // 'light' | 'dark' | 'system'
|
|
233
235
|
});
|
|
@@ -240,7 +242,7 @@ Inject custom CSS for fine-grained styling overrides.
|
|
|
240
242
|
```tsx
|
|
241
243
|
const { open } = useExpressCSV({
|
|
242
244
|
schema,
|
|
243
|
-
|
|
245
|
+
getSessionToken: async () => fetchSessionToken(),
|
|
244
246
|
importIdentifier: "user-import",
|
|
245
247
|
customCSS: `
|
|
246
248
|
.ecsv [data-step="upload"] {
|
|
@@ -260,7 +262,7 @@ Load custom fonts via the `fonts` option:
|
|
|
260
262
|
```tsx
|
|
261
263
|
const { open } = useExpressCSV({
|
|
262
264
|
schema,
|
|
263
|
-
|
|
265
|
+
getSessionToken: async () => fetchSessionToken(),
|
|
264
266
|
importIdentifier: "user-import",
|
|
265
267
|
fonts: {
|
|
266
268
|
title: { source: "google", name: "Space Grotesk", weights: [400, 600, 700] },
|
|
@@ -273,202 +275,50 @@ const { open } = useExpressCSV({
|
|
|
273
275
|
});
|
|
274
276
|
```
|
|
275
277
|
|
|
276
|
-
##
|
|
278
|
+
## Delivery
|
|
277
279
|
|
|
278
|
-
|
|
280
|
+
ExpressCSV delivers imported data through `onData`. Your app receives validated chunks in the browser and can forward them to your backend using whatever request shape fits your stack.
|
|
279
281
|
|
|
280
282
|
```tsx
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
function App() {
|
|
289
|
-
const { open } = useExpressCSV({
|
|
290
|
-
schema,
|
|
291
|
-
publishableKey: "your-publishable-key",
|
|
292
|
-
importIdentifier: "user-import",
|
|
293
|
-
title: "Import Users",
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
const handleImport = () => {
|
|
297
|
-
open({
|
|
298
|
-
webhook: {
|
|
299
|
-
url: "https://api.example.com/webhooks/csv-import",
|
|
300
|
-
method: "POST",
|
|
301
|
-
headers: {
|
|
302
|
-
Authorization: "Bearer your-api-token",
|
|
303
|
-
},
|
|
304
|
-
metadata: {
|
|
305
|
-
source: "react-app",
|
|
306
|
-
userId: "user-123",
|
|
307
|
-
},
|
|
308
|
-
},
|
|
309
|
-
onComplete: () => {
|
|
310
|
-
console.log("Webhook delivery initiated");
|
|
311
|
-
},
|
|
312
|
-
onError: (error) => {
|
|
313
|
-
console.error("Delivery error:", error);
|
|
283
|
+
open({
|
|
284
|
+
chunkSize: 500,
|
|
285
|
+
onData: async (chunk, next) => {
|
|
286
|
+
const response = await fetch("/api/import-users/chunks", {
|
|
287
|
+
method: "POST",
|
|
288
|
+
headers: {
|
|
289
|
+
"Content-Type": "application/json",
|
|
314
290
|
},
|
|
291
|
+
body: JSON.stringify({
|
|
292
|
+
sessionId: chunk.sessionId,
|
|
293
|
+
chunkIdempotencyKey: chunk.chunkIdempotencyKey,
|
|
294
|
+
records: chunk.records,
|
|
295
|
+
currentChunkIndex: chunk.currentChunkIndex,
|
|
296
|
+
totalChunks: chunk.totalChunks,
|
|
297
|
+
totalRecords: chunk.totalRecords,
|
|
298
|
+
}),
|
|
315
299
|
});
|
|
316
|
-
};
|
|
317
|
-
|
|
318
|
-
return <button onClick={handleImport}>Import CSV via Webhook</button>;
|
|
319
|
-
}
|
|
320
|
-
```
|
|
321
|
-
|
|
322
|
-
### Webhook Payload Structure
|
|
323
|
-
|
|
324
|
-
Each chunk is delivered as a JSON `POST` (or whichever method you configured) to your endpoint. The request body has this shape:
|
|
325
|
-
|
|
326
|
-
```typescript
|
|
327
|
-
interface WebhookPayload {
|
|
328
|
-
/** Imported records for this chunk, matching your schema */
|
|
329
|
-
records: Record<string, unknown>[];
|
|
330
|
-
/** 0-based index of the current chunk */
|
|
331
|
-
chunkIndex: number;
|
|
332
|
-
/** Total number of chunks in this delivery */
|
|
333
|
-
totalChunks: number;
|
|
334
|
-
/** Total number of records across all chunks */
|
|
335
|
-
totalRecords: number;
|
|
336
|
-
/** Present only if you passed `metadata` in `WebhookConfig` */
|
|
337
|
-
metadata?: Record<string, unknown>;
|
|
338
|
-
/** Delivery context added by ExpressCSV */
|
|
339
|
-
delivery: {
|
|
340
|
-
publishableKey: string;
|
|
341
|
-
environmentName: string;
|
|
342
|
-
environmentType: string;
|
|
343
|
-
teamSlug: string;
|
|
344
|
-
importIdentifier: string;
|
|
345
|
-
deliveryId: string;
|
|
346
|
-
timestamp: string; // ISO 8601
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
```
|
|
350
|
-
|
|
351
|
-
When you already have a schema, prefer `InferWebhookPayload<typeof schema>` from `@expresscsv/schemas` instead of rewriting this object shape by hand.
|
|
352
|
-
|
|
353
|
-
Example payload:
|
|
354
|
-
|
|
355
|
-
```json
|
|
356
|
-
{
|
|
357
|
-
"records": [
|
|
358
|
-
{ "name": "Alice Johnson", "email": "alice@example.com" },
|
|
359
|
-
{ "name": "Bob Smith", "email": "bob@example.com" }
|
|
360
|
-
],
|
|
361
|
-
"chunkIndex": 0,
|
|
362
|
-
"totalChunks": 3,
|
|
363
|
-
"totalRecords": 2500,
|
|
364
|
-
"metadata": {
|
|
365
|
-
"source": "react-app",
|
|
366
|
-
"userId": "user-123"
|
|
367
|
-
},
|
|
368
|
-
"delivery": {
|
|
369
|
-
"publishableKey": "pk_live_abc123",
|
|
370
|
-
"environmentName": "Production",
|
|
371
|
-
"environmentType": "production",
|
|
372
|
-
"teamSlug": "my-team",
|
|
373
|
-
"importIdentifier": "user-import",
|
|
374
|
-
"deliveryId": "del_abc123",
|
|
375
|
-
"timestamp": "2026-03-02T14:30:00.000Z"
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
```
|
|
379
|
-
|
|
380
|
-
The request includes a `Content-Type: application/json` header plus any custom `headers` you specified in `WebhookConfig`.
|
|
381
|
-
|
|
382
|
-
### Handling Webhooks on Your Server
|
|
383
|
-
|
|
384
|
-
Your endpoint should return a **2xx** status code to acknowledge each chunk. Non-2xx responses trigger the following retry behaviour:
|
|
385
|
-
|
|
386
|
-
- **5xx** and **429** responses are retried automatically (up to 5 attempts per chunk).
|
|
387
|
-
- **4xx** responses (except 429) are treated as permanent failures and are **not** retried.
|
|
388
|
-
|
|
389
|
-
Chunks are delivered **serially** — the next chunk is only sent after the previous one succeeds.
|
|
390
|
-
|
|
391
|
-
If your schema lives in shared or backend code via `@expresscsv/schemas`, you can reuse the built-in webhook payload helper in your webhook handler:
|
|
392
|
-
|
|
393
|
-
```typescript
|
|
394
|
-
import express from "express";
|
|
395
|
-
import type { InferWebhookPayload } from "@expresscsv/schemas";
|
|
396
|
-
import { candidateSchema } from "../shared/candidate-schema";
|
|
397
|
-
|
|
398
|
-
const app = express();
|
|
399
|
-
app.use(express.json());
|
|
400
|
-
|
|
401
|
-
app.post("/webhooks/csv-import", async (req, res) => {
|
|
402
|
-
const payload = req.body as InferWebhookPayload<typeof candidateSchema>;
|
|
403
|
-
|
|
404
|
-
await importCandidates(payload.records);
|
|
405
|
-
|
|
406
|
-
res.status(200).json({ ok: true });
|
|
407
|
-
});
|
|
408
|
-
```
|
|
409
|
-
|
|
410
|
-
Below is a minimal Express.js example:
|
|
411
|
-
|
|
412
|
-
```typescript
|
|
413
|
-
import express from "express";
|
|
414
|
-
|
|
415
|
-
const app = express();
|
|
416
|
-
app.use(express.json());
|
|
417
|
-
|
|
418
|
-
app.post("/webhooks/csv-import", async (req, res) => {
|
|
419
|
-
const token = req.headers.authorization;
|
|
420
|
-
if (token !== "Bearer your-api-token") {
|
|
421
|
-
return res.status(401).json({ error: "Unauthorized" });
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
const { records, chunkIndex, totalChunks, totalRecords, metadata, delivery } =
|
|
425
|
-
req.body;
|
|
426
|
-
|
|
427
|
-
try {
|
|
428
|
-
// Process the records — e.g. insert into your database
|
|
429
|
-
await db.insertMany("users", records);
|
|
430
|
-
|
|
431
|
-
console.log(
|
|
432
|
-
`Chunk ${chunkIndex + 1}/${totalChunks} processed ` +
|
|
433
|
-
`(${records.length} records, delivery ${delivery.deliveryId})`
|
|
434
|
-
);
|
|
435
|
-
|
|
436
|
-
res.status(200).json({ success: true });
|
|
437
|
-
} catch (error) {
|
|
438
|
-
// Return 500 so ExpressCSV retries this chunk
|
|
439
|
-
console.error("Failed to process chunk:", error);
|
|
440
|
-
res.status(500).json({ error: "Internal server error" });
|
|
441
|
-
}
|
|
442
|
-
});
|
|
443
300
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
**Tips:**
|
|
448
|
-
|
|
449
|
-
- Use `delivery.deliveryId` and `chunkIndex` to **deduplicate** retried chunks (the same chunk may be delivered more than once on retry).
|
|
450
|
-
- Use `chunkIndex` and `totalChunks` to track progress and know when the full import is complete (`chunkIndex === totalChunks - 1` for the last chunk).
|
|
451
|
-
- Store `metadata` alongside imported records if you need to correlate the import with a specific user or action in your app.
|
|
452
|
-
|
|
453
|
-
### Combined Local Callback and Webhook
|
|
301
|
+
if (!response.ok) {
|
|
302
|
+
throw new Error("Backend rejected this import chunk");
|
|
303
|
+
}
|
|
454
304
|
|
|
455
|
-
```tsx
|
|
456
|
-
open({
|
|
457
|
-
chunkSize: 500,
|
|
458
|
-
onData: async (chunk, next) => {
|
|
459
|
-
await saveToLocalDatabase(chunk.records);
|
|
460
305
|
next();
|
|
461
306
|
},
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
headers: { Authorization: "Bearer your-api-token" },
|
|
465
|
-
},
|
|
466
|
-
onComplete: () => {
|
|
467
|
-
console.log("Local processing and webhook delivery complete");
|
|
307
|
+
onComplete: ({ sessionId }) => {
|
|
308
|
+
console.log("Import complete for", sessionId);
|
|
468
309
|
},
|
|
469
310
|
});
|
|
470
311
|
```
|
|
471
312
|
|
|
313
|
+
Each delivered chunk includes:
|
|
314
|
+
|
|
315
|
+
- `records`
|
|
316
|
+
- `sessionId`
|
|
317
|
+
- `chunkIdempotencyKey`
|
|
318
|
+
- `currentChunkIndex`
|
|
319
|
+
- `totalChunks`
|
|
320
|
+
- `totalRecords`
|
|
321
|
+
|
|
472
322
|
## Advanced Example
|
|
473
323
|
|
|
474
324
|
```tsx
|
|
@@ -494,7 +344,7 @@ function CandidateImporter() {
|
|
|
494
344
|
|
|
495
345
|
const { open, isOpen } = useExpressCSV({
|
|
496
346
|
schema: candidateSchema,
|
|
497
|
-
|
|
347
|
+
getSessionToken: async () => fetchSessionToken(),
|
|
498
348
|
importIdentifier: "candidate-import",
|
|
499
349
|
title: "Import Candidates",
|
|
500
350
|
});
|
|
@@ -510,13 +360,14 @@ function CandidateImporter() {
|
|
|
510
360
|
await importCandidates(chunk.records);
|
|
511
361
|
next();
|
|
512
362
|
},
|
|
513
|
-
onComplete: () => {
|
|
514
|
-
setStatus(
|
|
363
|
+
onComplete: ({ sessionId }) => {
|
|
364
|
+
setStatus(`Import complete: ${sessionId}`);
|
|
515
365
|
},
|
|
516
|
-
onError: (error) => {
|
|
517
|
-
setStatus(`Error: ${error.message}`);
|
|
366
|
+
onError: (error, { sessionId }) => {
|
|
367
|
+
setStatus(`Error in ${sessionId}: ${error.message}`);
|
|
518
368
|
},
|
|
519
|
-
onCancel: () => {
|
|
369
|
+
onCancel: ({ sessionId }) => {
|
|
370
|
+
console.log("Cancelled session", sessionId);
|
|
520
371
|
setStatus(null);
|
|
521
372
|
},
|
|
522
373
|
});
|
|
@@ -542,7 +393,7 @@ function useExpressCSV<TSchema>(
|
|
|
542
393
|
options: UseExpressCSVOptions<TSchema>
|
|
543
394
|
): {
|
|
544
395
|
open: (options: OpenOptions<Infer<TSchema>>) => void;
|
|
545
|
-
|
|
396
|
+
importerState: ImporterState;
|
|
546
397
|
isInitialising: boolean;
|
|
547
398
|
isOpen: boolean;
|
|
548
399
|
};
|
|
@@ -553,48 +404,48 @@ function useExpressCSV<TSchema>(
|
|
|
553
404
|
| Option | Type | Required | Default | Description |
|
|
554
405
|
|---|---|---|---|---|
|
|
555
406
|
| `schema` | Schema | Yes | - | Schema definition created with `x.row()` |
|
|
556
|
-
| `
|
|
407
|
+
| `getSessionToken` | `() => Promise<string>` | Yes | - | Async callback that asks your backend for a short-lived importer session token |
|
|
557
408
|
| `importIdentifier` | `string` | Yes | - | Unique identifier for this import type |
|
|
558
|
-
| `title` | `string` | No | - | Title shown in the
|
|
559
|
-
| `preload` | `boolean` | No | `true` | Preload
|
|
409
|
+
| `title` | `string` | No | - | Title shown in the importer header |
|
|
410
|
+
| `preload` | `boolean` | No | `true` | Preload the importer for instant display |
|
|
560
411
|
| `debug` | `boolean` | No | `false` | Enable debug logging |
|
|
561
412
|
| `theme` | `ECSVTheme` | No | - | Custom theme configuration |
|
|
562
413
|
| `colorMode` | `ColorModePref` | No | - | Light/dark mode (`'light'`, `'dark'`, or `'system'`) |
|
|
563
|
-
| `customCSS` | `string` | No | - | Custom CSS to inject into the
|
|
414
|
+
| `customCSS` | `string` | No | - | Custom CSS to inject into the importer |
|
|
564
415
|
| `fonts` | `Record<string, ECSVFontSource>` | No | - | Custom font sources |
|
|
565
416
|
| `stepDisplay` | `'progressBar' \| 'segmented' \| 'numbered'` | No | `'progressBar'` | Step indicator style |
|
|
566
417
|
| `previewSchemaBeforeUpload` | `boolean` | No | `true` | Show schema preview before upload |
|
|
418
|
+
| `aiColumnMatching` | `boolean` | No | `true` | Enable AI-assisted column matching |
|
|
419
|
+
| `aiTransform` | `boolean` | No | `true` | Enable AI-assisted transform generation |
|
|
567
420
|
| `templateDownload` | `TemplateDownloadOptions<TSchema>` | No | - | Template download configuration with optional schema-typed example rows |
|
|
568
|
-
| `
|
|
421
|
+
| `storage` | `{ type: "local" } \| { type: "custom", ... }` | No | - | Enable Recovered Sessions with the built-in local backend or a custom adapter implementing `get`, `set`, and `remove` |
|
|
569
422
|
| `locale` | `DeepPartial<ExpressCSVLocaleInput>` | No | - | Localization overrides |
|
|
423
|
+
| `disableStatusStep` | `boolean` | No | - | Skip the success/error status screen |
|
|
570
424
|
|
|
571
425
|
#### Return Value
|
|
572
426
|
|
|
573
427
|
| Property | Type | Description |
|
|
574
428
|
|---|---|---|
|
|
575
|
-
| `open` | `(options: OpenOptions) => void` | Opens the
|
|
576
|
-
| `
|
|
577
|
-
| `isInitialising` | `boolean` | `true` while the
|
|
578
|
-
| `isOpen` | `boolean` | `true` while the
|
|
429
|
+
| `open` | `(options: OpenOptions) => void` | Opens the importer. Requires `onData` for delivery. |
|
|
430
|
+
| `importerState` | `ImporterState` | Current importer lifecycle state (reactive) |
|
|
431
|
+
| `isInitialising` | `boolean` | `true` while the importer is initializing or opening |
|
|
432
|
+
| `isOpen` | `boolean` | `true` while the importer is open |
|
|
579
433
|
|
|
580
434
|
### `OpenOptions<T>`
|
|
581
435
|
|
|
582
|
-
Options passed to `open()`.
|
|
436
|
+
Options passed to `open()`.
|
|
583
437
|
|
|
584
438
|
| Option | Type | Required | Description |
|
|
585
439
|
|---|---|---|---|
|
|
586
|
-
| `onData` | `(chunk: RecordsChunk<T>, next: () => void) => void` |
|
|
587
|
-
| `webhook` | `WebhookConfig` | * | Webhook endpoint for server-side delivery |
|
|
440
|
+
| `onData` | `(chunk: RecordsChunk<T>, next: () => void) => void` | Yes | Callback for each delivered chunk. Call `next()` to continue. |
|
|
588
441
|
| `chunkSize` | `number` | No | Records per chunk (default: 1000) |
|
|
589
|
-
| `onComplete` | `() => void` | No | Called when all chunks have been processed |
|
|
590
|
-
| `onCancel` | `() => void` | No | Called when the user cancels the import |
|
|
591
|
-
| `onError` | `(error: Error) => void` | No | Called when an error occurs |
|
|
592
|
-
| `
|
|
593
|
-
| `
|
|
442
|
+
| `onComplete` | `(context: { sessionId: string }) => void` | No | Called when all chunks have been processed |
|
|
443
|
+
| `onCancel` | `(context: { sessionId: string }) => void` | No | Called when the user cancels the import |
|
|
444
|
+
| `onError` | `(error: Error, context: { sessionId: string }) => void` | No | Called when an error occurs |
|
|
445
|
+
| `onImporterOpen` | `() => void` | No | Called when the importer opens |
|
|
446
|
+
| `onImporterClose` | `(reason: 'user_close' \| 'cancel' \| 'complete' \| 'error') => void` | No | Called when the importer closes |
|
|
594
447
|
| `onStepChange` | `(stepId, previousStepId?) => void` | No | Called when the wizard step changes |
|
|
595
448
|
|
|
596
|
-
\* At least one of `onData` or `webhook` is required.
|
|
597
|
-
|
|
598
449
|
### `RecordsChunk<T>`
|
|
599
450
|
|
|
600
451
|
```typescript
|
|
@@ -603,19 +454,8 @@ interface RecordsChunk<T> {
|
|
|
603
454
|
totalChunks: number;
|
|
604
455
|
currentChunkIndex: number;
|
|
605
456
|
totalRecords: number;
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
### `WebhookConfig`
|
|
610
|
-
|
|
611
|
-
```typescript
|
|
612
|
-
interface WebhookConfig {
|
|
613
|
-
url: string;
|
|
614
|
-
headers?: Record<string, string>;
|
|
615
|
-
method?: "POST" | "PUT" | "PATCH";
|
|
616
|
-
timeout?: number;
|
|
617
|
-
retries?: number;
|
|
618
|
-
metadata?: Record<string, unknown>;
|
|
457
|
+
sessionId: string;
|
|
458
|
+
chunkIdempotencyKey: string;
|
|
619
459
|
}
|
|
620
460
|
```
|
|
621
461
|
|
|
@@ -623,7 +463,7 @@ interface WebhookConfig {
|
|
|
623
463
|
|
|
624
464
|
The `x` schema builder provides a type-safe, fluent API for defining your CSV structure.
|
|
625
465
|
|
|
626
|
-
For apps that share schemas with backend code
|
|
466
|
+
For apps that share schemas with backend code, prefer defining the schema in `@expresscsv/schemas` and importing it into your React app. That keeps schema authoring free of React and importer dependencies.
|
|
627
467
|
|
|
628
468
|
#### Field Types
|
|
629
469
|
|
|
@@ -728,7 +568,7 @@ All field types support:
|
|
|
728
568
|
|
|
729
569
|
| Modifier | Description |
|
|
730
570
|
|---|---|
|
|
731
|
-
| `.label(text)` | User-facing label shown in the
|
|
571
|
+
| `.label(text)` | User-facing label shown in the importer |
|
|
732
572
|
| `.description(text)` | Help text for the field |
|
|
733
573
|
| `.example(text)` | Example value shown as placeholder |
|
|
734
574
|
| `.optional()` | Makes the field optional (default is required) |
|
|
@@ -780,7 +620,7 @@ x.string().refine(
|
|
|
780
620
|
)
|
|
781
621
|
```
|
|
782
622
|
|
|
783
|
-
**Object-returning validator** — inline `valid`, `message`, and an optional `suggestedFix` the
|
|
623
|
+
**Object-returning validator** — inline `valid`, `message`, and an optional `suggestedFix` the importer can offer the user:
|
|
784
624
|
|
|
785
625
|
```tsx
|
|
786
626
|
x.string().refine((value) => ({
|
|
@@ -847,7 +687,7 @@ x.string().refineBatch((values) => {
|
|
|
847
687
|
|
|
848
688
|
### `suggestedFix`
|
|
849
689
|
|
|
850
|
-
Both `.refine()` and `.refineBatch()` support returning a `suggestedFix` object that the
|
|
690
|
+
Both `.refine()` and `.refineBatch()` support returning a `suggestedFix` object that the importer offers as a one-click fix for the user:
|
|
851
691
|
|
|
852
692
|
```tsx
|
|
853
693
|
interface SuggestedFix {
|