@expresscsv/react 0.1.25 → 1.0.0
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 +63 -53
- package/dist/index.d.cts +248 -124
- package/dist/index.d.mts +248 -124
- package/dist/index.d.ts +248 -124
- package/dist/index.js +2 -2
- package/dist/index.mjs +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ yarn add @expresscsv/react
|
|
|
25
25
|
## Quick Start
|
|
26
26
|
|
|
27
27
|
```tsx
|
|
28
|
-
import { useExpressCSV, x } from "@expresscsv/react";
|
|
28
|
+
import { ImporterStatus, useExpressCSV, x } from "@expresscsv/react";
|
|
29
29
|
|
|
30
30
|
const schema = x.row({
|
|
31
31
|
name: x.string().label("Full Name"),
|
|
@@ -34,26 +34,26 @@ const schema = x.row({
|
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
function App() {
|
|
37
|
-
const { open,
|
|
37
|
+
const { open, status } = useExpressCSV({
|
|
38
38
|
schema,
|
|
39
39
|
getSessionToken: async () => fetchSessionToken(),
|
|
40
|
-
|
|
40
|
+
importNamespace: "user-import",
|
|
41
41
|
title: "Import Users",
|
|
42
42
|
});
|
|
43
43
|
|
|
44
44
|
const handleImport = () => {
|
|
45
45
|
open({
|
|
46
|
-
chunkSize: 500,
|
|
46
|
+
chunkSize: { unit: "kb", value: 500 },
|
|
47
47
|
onData: async (chunk, next) => {
|
|
48
48
|
console.log(
|
|
49
|
-
`Chunk ${chunk.
|
|
49
|
+
`Chunk ${chunk.chunkIndex + 1}/${chunk.totalChunks}`
|
|
50
50
|
);
|
|
51
51
|
console.log("Session:", chunk.sessionId);
|
|
52
|
-
console.log("
|
|
52
|
+
console.log("Delivery ID:", chunk.deliveryId);
|
|
53
53
|
console.log("Records:", chunk.records);
|
|
54
54
|
next();
|
|
55
55
|
},
|
|
56
|
-
onComplete: ({ sessionId }) => {
|
|
56
|
+
onComplete: ({ sessionId, deliveryId }) => {
|
|
57
57
|
console.log("All chunks processed for", sessionId);
|
|
58
58
|
},
|
|
59
59
|
onCancel: ({ sessionId }) => {
|
|
@@ -66,7 +66,7 @@ function App() {
|
|
|
66
66
|
};
|
|
67
67
|
|
|
68
68
|
return (
|
|
69
|
-
<button onClick={handleImport} disabled={
|
|
69
|
+
<button onClick={handleImport} disabled={status === ImporterStatus.OPEN}>
|
|
70
70
|
Import CSV
|
|
71
71
|
</button>
|
|
72
72
|
);
|
|
@@ -83,7 +83,7 @@ By default the importer preloads in a hidden iframe so it appears instantly when
|
|
|
83
83
|
const { open } = useExpressCSV({
|
|
84
84
|
schema,
|
|
85
85
|
getSessionToken: async () => fetchSessionToken(),
|
|
86
|
-
|
|
86
|
+
importNamespace: "user-import",
|
|
87
87
|
});
|
|
88
88
|
|
|
89
89
|
// Importer displays instantly
|
|
@@ -103,7 +103,7 @@ To disable preloading (there will be a brief loading screen instead):
|
|
|
103
103
|
const { open } = useExpressCSV({
|
|
104
104
|
schema,
|
|
105
105
|
getSessionToken: async () => fetchSessionToken(),
|
|
106
|
-
|
|
106
|
+
importNamespace: "user-import",
|
|
107
107
|
preload: false,
|
|
108
108
|
});
|
|
109
109
|
```
|
|
@@ -127,7 +127,7 @@ const candidateSchema = x.row({
|
|
|
127
127
|
const { open } = useExpressCSV({
|
|
128
128
|
schema: candidateSchema,
|
|
129
129
|
getSessionToken: async () => fetchSessionToken(),
|
|
130
|
-
|
|
130
|
+
importNamespace: "candidate-import",
|
|
131
131
|
templateDownload: {
|
|
132
132
|
source: "generate",
|
|
133
133
|
formats: ["csv", "xlsx"],
|
|
@@ -151,10 +151,10 @@ Customize the importer's appearance with the `theme`, `colorMode`, `customCSS`,
|
|
|
151
151
|
Use the `theme` option to override CSS variables (colors, radius, typography). Pass either a single theme (applies to both light and dark) or a dual-mode theme with separate light/dark values:
|
|
152
152
|
|
|
153
153
|
```tsx
|
|
154
|
-
import { useExpressCSV, x, type
|
|
154
|
+
import { useExpressCSV, x, type Theme } from "@expresscsv/react";
|
|
155
155
|
|
|
156
156
|
// Single theme (both modes)
|
|
157
|
-
const theme:
|
|
157
|
+
const theme: Theme = {
|
|
158
158
|
primary: "#4F46E5",
|
|
159
159
|
"primary-foreground": "#ffffff",
|
|
160
160
|
background: "#ffffff",
|
|
@@ -165,7 +165,7 @@ const theme: ECSVTheme = {
|
|
|
165
165
|
};
|
|
166
166
|
|
|
167
167
|
// Dual-mode (light and dark)
|
|
168
|
-
const dualTheme:
|
|
168
|
+
const dualTheme: Theme = {
|
|
169
169
|
modes: {
|
|
170
170
|
light: {
|
|
171
171
|
primary: "#4F46E5",
|
|
@@ -184,7 +184,7 @@ function App() {
|
|
|
184
184
|
const { open } = useExpressCSV({
|
|
185
185
|
schema,
|
|
186
186
|
getSessionToken: async () => fetchSessionToken(),
|
|
187
|
-
|
|
187
|
+
importNamespace: "user-import",
|
|
188
188
|
theme,
|
|
189
189
|
});
|
|
190
190
|
// ...
|
|
@@ -230,7 +230,7 @@ Control light/dark mode with `colorMode`:
|
|
|
230
230
|
const { open } = useExpressCSV({
|
|
231
231
|
schema,
|
|
232
232
|
getSessionToken: async () => fetchSessionToken(),
|
|
233
|
-
|
|
233
|
+
importNamespace: "user-import",
|
|
234
234
|
colorMode: "system", // 'light' | 'dark' | 'system'
|
|
235
235
|
});
|
|
236
236
|
```
|
|
@@ -243,7 +243,7 @@ Inject custom CSS for fine-grained styling overrides.
|
|
|
243
243
|
const { open } = useExpressCSV({
|
|
244
244
|
schema,
|
|
245
245
|
getSessionToken: async () => fetchSessionToken(),
|
|
246
|
-
|
|
246
|
+
importNamespace: "user-import",
|
|
247
247
|
customCSS: `
|
|
248
248
|
.ecsv [data-step="upload"] {
|
|
249
249
|
border-radius: 1rem;
|
|
@@ -263,7 +263,7 @@ Load custom fonts via the `fonts` option:
|
|
|
263
263
|
const { open } = useExpressCSV({
|
|
264
264
|
schema,
|
|
265
265
|
getSessionToken: async () => fetchSessionToken(),
|
|
266
|
-
|
|
266
|
+
importNamespace: "user-import",
|
|
267
267
|
fonts: {
|
|
268
268
|
title: { source: "google", name: "Space Grotesk", weights: [400, 600, 700] },
|
|
269
269
|
body: { source: "custom", url: "https://example.com/font.woff2", format: "woff2" },
|
|
@@ -281,18 +281,19 @@ ExpressCSV delivers imported data through `onData`. Your app receives validated
|
|
|
281
281
|
|
|
282
282
|
```tsx
|
|
283
283
|
open({
|
|
284
|
-
chunkSize: 500,
|
|
284
|
+
chunkSize: { unit: "kb", value: 500 },
|
|
285
285
|
onData: async (chunk, next) => {
|
|
286
|
-
const response = await fetch("/api/import-users/chunks", {
|
|
286
|
+
const response = await fetch("/your-api/import-users/chunks", {
|
|
287
287
|
method: "POST",
|
|
288
288
|
headers: {
|
|
289
289
|
"Content-Type": "application/json",
|
|
290
|
+
Authorization: `Bearer ${accessToken}`,
|
|
290
291
|
},
|
|
291
292
|
body: JSON.stringify({
|
|
292
293
|
sessionId: chunk.sessionId,
|
|
293
|
-
|
|
294
|
+
deliveryId: chunk.deliveryId,
|
|
295
|
+
chunkIndex: chunk.chunkIndex,
|
|
294
296
|
records: chunk.records,
|
|
295
|
-
currentChunkIndex: chunk.currentChunkIndex,
|
|
296
297
|
totalChunks: chunk.totalChunks,
|
|
297
298
|
totalRecords: chunk.totalRecords,
|
|
298
299
|
}),
|
|
@@ -304,8 +305,15 @@ open({
|
|
|
304
305
|
|
|
305
306
|
next();
|
|
306
307
|
},
|
|
307
|
-
onComplete: ({ sessionId }) => {
|
|
308
|
-
|
|
308
|
+
onComplete: async ({ sessionId, deliveryId }) => {
|
|
309
|
+
await fetch("/your-api/import-users/complete", {
|
|
310
|
+
method: "POST",
|
|
311
|
+
headers: {
|
|
312
|
+
"Content-Type": "application/json",
|
|
313
|
+
Authorization: `Bearer ${accessToken}`,
|
|
314
|
+
},
|
|
315
|
+
body: JSON.stringify({ sessionId, deliveryId }),
|
|
316
|
+
});
|
|
309
317
|
},
|
|
310
318
|
});
|
|
311
319
|
```
|
|
@@ -314,15 +322,18 @@ Each delivered chunk includes:
|
|
|
314
322
|
|
|
315
323
|
- `records`
|
|
316
324
|
- `sessionId`
|
|
317
|
-
- `
|
|
318
|
-
- `
|
|
325
|
+
- `deliveryId`
|
|
326
|
+
- `chunkIndex`
|
|
319
327
|
- `totalChunks`
|
|
320
328
|
- `totalRecords`
|
|
321
329
|
|
|
330
|
+
Each time the user finishes the import, ExpressCSV creates a new `deliveryId` for that `sessionId`. Your `onData` code can fail, and the user can retry delivery without starting the import over. Store chunks by `(sessionId, deliveryId, chunkIndex)`, then finalize the `deliveryId` from `onComplete` after all of its chunks are accepted.
|
|
331
|
+
|
|
332
|
+
|
|
322
333
|
## Advanced Example
|
|
323
334
|
|
|
324
335
|
```tsx
|
|
325
|
-
import { useExpressCSV, x, type Infer } from "@expresscsv/react";
|
|
336
|
+
import { ImporterStatus, useExpressCSV, x, type Infer } from "@expresscsv/react";
|
|
326
337
|
import { useState } from "react";
|
|
327
338
|
|
|
328
339
|
const candidateSchema = x.row({
|
|
@@ -342,10 +353,10 @@ const candidateSchema = x.row({
|
|
|
342
353
|
function CandidateImporter() {
|
|
343
354
|
const [status, setStatus] = useState<string | null>(null);
|
|
344
355
|
|
|
345
|
-
const { open,
|
|
356
|
+
const { open, status: importerStatus } = useExpressCSV({
|
|
346
357
|
schema: candidateSchema,
|
|
347
358
|
getSessionToken: async () => fetchSessionToken(),
|
|
348
|
-
|
|
359
|
+
importNamespace: "candidate-import",
|
|
349
360
|
title: "Import Candidates",
|
|
350
361
|
});
|
|
351
362
|
|
|
@@ -355,12 +366,12 @@ function CandidateImporter() {
|
|
|
355
366
|
open({
|
|
356
367
|
onData: async (chunk, next) => {
|
|
357
368
|
setStatus(
|
|
358
|
-
`Processing chunk ${chunk.
|
|
369
|
+
`Processing chunk ${chunk.chunkIndex + 1}/${chunk.totalChunks}...`
|
|
359
370
|
);
|
|
360
371
|
await importCandidates(chunk.records);
|
|
361
372
|
next();
|
|
362
373
|
},
|
|
363
|
-
onComplete: ({ sessionId }) => {
|
|
374
|
+
onComplete: ({ sessionId, deliveryId }) => {
|
|
364
375
|
setStatus(`Import complete: ${sessionId}`);
|
|
365
376
|
},
|
|
366
377
|
onError: (error, { sessionId }) => {
|
|
@@ -375,8 +386,13 @@ function CandidateImporter() {
|
|
|
375
386
|
|
|
376
387
|
return (
|
|
377
388
|
<div>
|
|
378
|
-
<button
|
|
379
|
-
{
|
|
389
|
+
<button
|
|
390
|
+
onClick={handleImport}
|
|
391
|
+
disabled={importerStatus === ImporterStatus.OPEN}
|
|
392
|
+
>
|
|
393
|
+
{importerStatus === ImporterStatus.OPEN
|
|
394
|
+
? "Importing..."
|
|
395
|
+
: "Import Candidates"}
|
|
380
396
|
</button>
|
|
381
397
|
{status && <p>{status}</p>}
|
|
382
398
|
</div>
|
|
@@ -393,9 +409,7 @@ function useExpressCSV<TSchema>(
|
|
|
393
409
|
options: UseExpressCSVOptions<TSchema>
|
|
394
410
|
): {
|
|
395
411
|
open: (options: OpenOptions<Infer<TSchema>>) => void;
|
|
396
|
-
|
|
397
|
-
isInitialising: boolean;
|
|
398
|
-
isOpen: boolean;
|
|
412
|
+
status: ImporterStatus;
|
|
399
413
|
};
|
|
400
414
|
```
|
|
401
415
|
|
|
@@ -405,20 +419,20 @@ function useExpressCSV<TSchema>(
|
|
|
405
419
|
|---|---|---|---|---|
|
|
406
420
|
| `schema` | Schema | Yes | - | Schema definition created with `x.row()` |
|
|
407
421
|
| `getSessionToken` | `() => Promise<string>` | Yes | - | Async callback that asks your backend for a short-lived importer session token |
|
|
408
|
-
| `
|
|
422
|
+
| `importNamespace` | `string` | Yes | - | Stable namespace string your app assigns to this importer configuration. Keep it the same for the same workflow; use a different value for different importers. |
|
|
409
423
|
| `title` | `string` | No | - | Title shown in the importer header |
|
|
410
424
|
| `preload` | `boolean` | No | `true` | Preload the importer for instant display |
|
|
411
425
|
| `debug` | `boolean` | No | `false` | Enable debug logging |
|
|
412
|
-
| `theme` | `
|
|
426
|
+
| `theme` | `Theme` | No | - | Custom theme configuration |
|
|
413
427
|
| `colorMode` | `ColorModePref` | No | - | Light/dark mode (`'light'`, `'dark'`, or `'system'`) |
|
|
414
428
|
| `customCSS` | `string` | No | - | Custom CSS to inject into the importer |
|
|
415
|
-
| `fonts` | `Record<string,
|
|
429
|
+
| `fonts` | `Record<string, FontSource>` | No | - | Custom font sources |
|
|
416
430
|
| `stepDisplay` | `'progressBar' \| 'segmented' \| 'numbered'` | No | `'progressBar'` | Step indicator style |
|
|
417
431
|
| `previewSchemaBeforeUpload` | `boolean` | No | `true` | Show schema preview before upload |
|
|
418
|
-
| `
|
|
419
|
-
| `
|
|
432
|
+
| `columnMatching` | `{ type: "managed"; exact?: boolean; caseInsensitive?: boolean; normalized?: boolean; inference?: boolean } \| { type: "custom"; columnMatchHandler: (...) => Promise<...> }` | No | `undefined` | Configure managed column matching or provide a custom matcher |
|
|
433
|
+
| `promptedEdits` | `{ type: "managed" } \| { type: "custom"; promptedEditHandler: (...) => Promise<...> }` | No | `undefined` | Enable managed prompted edits or provide a custom edit handler |
|
|
420
434
|
| `templateDownload` | `TemplateDownloadOptions<TSchema>` | No | - | Template download configuration with optional schema-typed example rows |
|
|
421
|
-
| `
|
|
435
|
+
| `sessionRecovery` | `SessionRecoveryOptions` | No | - | Enable Recovered sessions with the built-in local backend or a custom adapter implementing `get`, `set`, and `remove` |
|
|
422
436
|
| `locale` | `DeepPartial<ExpressCSVLocaleInput>` | No | - | Localization overrides |
|
|
423
437
|
| `disableStatusStep` | `boolean` | No | - | Skip the success/error status screen |
|
|
424
438
|
|
|
@@ -427,9 +441,7 @@ function useExpressCSV<TSchema>(
|
|
|
427
441
|
| Property | Type | Description |
|
|
428
442
|
|---|---|---|
|
|
429
443
|
| `open` | `(options: OpenOptions) => void` | Opens the importer. Requires `onData` for delivery. |
|
|
430
|
-
| `
|
|
431
|
-
| `isInitialising` | `boolean` | `true` while the importer is initializing or opening |
|
|
432
|
-
| `isOpen` | `boolean` | `true` while the importer is open |
|
|
444
|
+
| `status` | `ImporterStatus` | Current importer lifecycle status (reactive) |
|
|
433
445
|
|
|
434
446
|
### `OpenOptions<T>`
|
|
435
447
|
|
|
@@ -437,14 +449,13 @@ Options passed to `open()`.
|
|
|
437
449
|
|
|
438
450
|
| Option | Type | Required | Description |
|
|
439
451
|
|---|---|---|---|
|
|
440
|
-
| `onData` | `(chunk: RecordsChunk<T>, next: () => void) => void` | Yes | Callback for each delivered chunk. Call `next()`
|
|
441
|
-
| `chunkSize` | `
|
|
452
|
+
| `onData` | `(chunk: RecordsChunk<T>, next: () => void) => void` | Yes | Callback for each delivered chunk. Call `next()` after your backend accepts the current chunk. |
|
|
453
|
+
| `chunkSize` | `ChunkSize` | No | Delivery packet size. Defaults to `{ unit: "kb", value: 500 }`. `kb` uses decimal kilobytes (`1 KB = 1000 bytes`). Use `{ unit: "kb", value: 500 }` for KB or `{ unit: "rows", value: 500 }` for row counts. Zero or negative values send all records in one chunk. |
|
|
442
454
|
| `onComplete` | `(context: { sessionId: string }) => void` | No | Called when all chunks have been processed |
|
|
443
455
|
| `onCancel` | `(context: { sessionId: string }) => void` | No | Called when the user cancels the import |
|
|
444
456
|
| `onError` | `(error: Error, context: { sessionId: string }) => void` | No | Called when an error occurs |
|
|
445
|
-
| `
|
|
446
|
-
| `
|
|
447
|
-
| `onStepChange` | `(stepId, previousStepId?) => void` | No | Called when the wizard step changes |
|
|
457
|
+
| `onOpen` | `(context: { sessionId: string }) => void` | No | Called when the importer opens |
|
|
458
|
+
| `onStepChange` | `(stepId, previousStepId?) => void` | No | Called when the importer step changes |
|
|
448
459
|
|
|
449
460
|
### `RecordsChunk<T>`
|
|
450
461
|
|
|
@@ -452,10 +463,10 @@ Options passed to `open()`.
|
|
|
452
463
|
interface RecordsChunk<T> {
|
|
453
464
|
records: T[]; // Automatically typed to your schema
|
|
454
465
|
totalChunks: number;
|
|
455
|
-
|
|
466
|
+
chunkIndex: number;
|
|
456
467
|
totalRecords: number;
|
|
457
468
|
sessionId: string;
|
|
458
|
-
|
|
469
|
+
deliveryId: string;
|
|
459
470
|
}
|
|
460
471
|
```
|
|
461
472
|
|
|
@@ -570,7 +581,6 @@ All field types support:
|
|
|
570
581
|
|---|---|
|
|
571
582
|
| `.label(text)` | User-facing label shown in the importer |
|
|
572
583
|
| `.description(text)` | Help text for the field |
|
|
573
|
-
| `.example(text)` | Example value shown as placeholder |
|
|
574
584
|
| `.optional()` | Makes the field optional (default is required) |
|
|
575
585
|
| `.refine(fn, params?)` | Custom per-value validation — any sync or async JS function |
|
|
576
586
|
| `.refineBatch(fn, params?)` | Custom whole-column validation — receives all values at once |
|