@updog/data-editor 0.1.3 → 0.1.5

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.
Files changed (4) hide show
  1. package/README.md +281 -107
  2. package/index.css +1 -1
  3. package/index.d.ts +303 -53
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -1,11 +1,13 @@
1
1
  # @updog/data-editor
2
2
 
3
- Enterprise-grade spreadsheet editor SDK for React. Designed for editing large datasets (100k+ rows) with multi-source data merging, file imports, validation, and undo/redo.
3
+ Client-side data importer and spreadsheet editor SDK for React. Your users import files, match columns to your schema, fix errors, and submit clean data. Edits happen inline, in the browser, at 1M+ rows.
4
+
5
+ > **Not using React?** Use the framework-agnostic Web Component build — [`@updog/data-editor-wc`](https://www.npmjs.com/package/@updog/data-editor-wc) — for Angular, Vue, Svelte, or vanilla JS.
4
6
 
5
7
  ## Requirements
6
8
 
7
9
  - React 18.x or 19.x
8
- - An API key — this is a **commercial SDK**. Sign up at [updog.tech](https://updog.tech) to create an account and get a key. The editor will not render without one.
10
+ - An API key — this is a **commercial SDK**. Sign up at [updog.tech](https://updog.tech) to get one.
9
11
 
10
12
  ## Installation
11
13
 
@@ -16,44 +18,71 @@ npm install @updog/data-editor
16
18
  ## Quick Start
17
19
 
18
20
  ```tsx
19
- import { DataEditor } from "@updog/data-editor";
21
+ import { useState } from "react";
22
+ import { DataEditor, required, email } from "@updog/data-editor";
20
23
  import "@updog/data-editor/styles.css";
24
+ import type { DataEditorColumn } from "@updog/data-editor";
21
25
 
22
- function App() {
23
- const [open, setOpen] = useState(false);
24
-
25
- const columns = [
26
- { id: "name", title: "Name", width: 150 },
27
- { id: "email", title: "Email", width: 260 },
28
- ];
26
+ type Employee = {
27
+ id: string;
28
+ name: string;
29
+ email: string;
30
+ role: string;
31
+ };
29
32
 
30
- const loadData = async (onChunk) => {
31
- const res = await fetch("/api/employees");
32
- onChunk(await res.json());
33
- };
33
+ const columns: DataEditorColumn[] = [
34
+ { id: "name", title: "Full Name", size: 200, validate: required("Name is required") },
35
+ { id: "email", title: "Email", size: 250, validate: [required("Email is required"), email("Invalid email")] },
36
+ { id: "role", title: "Role", editor: { type: "select", options: ["Admin", "Editor", "Viewer"] } },
37
+ ];
34
38
 
35
- const onComplete = (result) => {
36
- console.log(result.valid, result.invalid, result.deleted);
37
- setOpen(false);
38
- };
39
+ export function App() {
40
+ const [open, setOpen] = useState(false);
39
41
 
40
42
  return (
41
43
  <>
42
44
  <button onClick={() => setOpen(true)}>Open Editor</button>
43
- <DataEditor
44
- apiKey="your-api-key"
45
+ <DataEditor<Employee>
46
+ apiKey="your-license-key"
45
47
  open={open}
46
48
  onClose={() => setOpen(false)}
47
49
  columns={columns}
48
50
  primaryKey="id"
49
- loadData={loadData}
50
- onComplete={onComplete}
51
+ loadData={async (onChunk) => {
52
+ const res = await fetch("/api/employees");
53
+ onChunk(await res.json());
54
+ }}
55
+ onComplete={async (result, actions) => {
56
+ for (const source of result.sources) {
57
+ const inserts = source.rows.filter((r) => r.isNew && !r.isDeleted && r.isValid);
58
+ const updates = source.rows.filter((r) => !r.isNew && r.isChanged && !r.isDeleted && r.isValid);
59
+ const deletes = source.rows.filter((r) => r.isDeleted && !r.isNew);
60
+ await persist(source.sourceId, { inserts, updates, deletes });
61
+ }
62
+ actions.reset();
63
+ setOpen(false);
64
+ }}
51
65
  />
52
66
  </>
53
67
  );
54
68
  }
55
69
  ```
56
70
 
71
+ ## TypeScript
72
+
73
+ `DataEditor` accepts a generic type parameter for row data. This gives you type-safe access to `primaryKey` and the result rows in `onComplete`.
74
+
75
+ ```tsx
76
+ type Employee = { id: string; name: string; email: string };
77
+
78
+ <DataEditor<Employee>
79
+ primaryKey="id" // autocomplete shows "id" | "name" | "email"
80
+ // ...
81
+ />
82
+ ```
83
+
84
+ When you omit the generic, rows are typed as `Record<string, unknown>`.
85
+
57
86
  ## Inline Mode
58
87
 
59
88
  Render the editor directly in the DOM without a modal overlay:
@@ -61,7 +90,7 @@ Render the editor directly in the DOM without a modal overlay:
61
90
  ```tsx
62
91
  <DataEditor
63
92
  mode="inline"
64
- apiKey="your-api-key"
93
+ apiKey="your-license-key"
65
94
  columns={columns}
66
95
  primaryKey="id"
67
96
  loadData={loadData}
@@ -69,151 +98,296 @@ Render the editor directly in the DOM without a modal overlay:
69
98
  />
70
99
  ```
71
100
 
101
+ In inline mode, `open` and `onClose` don't apply.
102
+
72
103
  ## Props
73
104
 
74
105
  | Prop | Type | Required | Default | Description |
75
106
  |---|---|---|---|---|
76
- | `mode` | `"modal"` \| `"inline"` | No | `"modal"` | Rendering mode |
77
- | `apiKey` | `string` | Yes | — | License key |
78
- | `open` | `boolean` | Modal only | — | Controls modal visibility. Required in modal mode only |
79
- | `onClose` | `() => void` | Modal only | | Called when user initiates close. Required in modal mode only |
80
- | `columns` | `DataEditorColumn[]` | Yes | — | Column definitions |
81
- | `primaryKey` | `keyof TRow` | Yes | — | Unique row identifier column |
82
- | `loadData` | `(onChunk) => Promise<void>` | No | — | Async data loader with chunked delivery |
83
- | `onComplete` | `(result, actions) => void` | No | — | Called on submit with valid/invalid/deleted rows |
84
- | `variant` | `"editor"` \| `"uploader"` | No | `"editor"` | UI mode |
85
- | `translations` | `DataEditorTranslations` | No | — | i18n overrides |
86
- | `locale` | `string` | No | `"en"` | BCP 47 locale tag |
87
- | `enableDeleteRow` | `"all"` \| `"new"` \| `false` | No | `false` | Row deletion policy |
88
- | `enableAddRow` | `boolean` | No | `true` | Show "Add row" option |
89
- | `importFormats` | `DataEditorFormat[]` | No | all | Allowed import formats |
90
- | `exportFormats` | `DataEditorFormat[]` | No | all | Allowed export formats |
91
- | `rowHeight` | `number` | No | `34` | Row height in pixels |
92
- | `headerHeight` | `number` | No | `36` | Header height in pixels |
93
- | `readonly` | `boolean` | No | `false` | Hide write-oriented UI |
94
- | `rtl` | `boolean` | No | `false` | Right-to-left layout |
95
- | `className` | `string` | No | | CSS class for the modal |
107
+ | `apiKey` | `string` | Yes | | Your Updog license key. Validated on each open. |
108
+ | `columns` | `DataEditorColumn[]` | Yes | — | Column definitions. |
109
+ | `primaryKey` | `keyof TRow` | Yes | — | Column ID that uniquely identifies each row. |
110
+ | `mode` | `"modal"` \| `"inline"` | No | `"modal"` | Rendering mode. |
111
+ | `open` | `boolean` | Modal only | — | Controlled modal visibility. |
112
+ | `onClose` | `() => void` | Modal only | — | Called when the user closes the modal (X button or Escape). |
113
+ | `loadData` | `(onChunk) => Promise<void>` | No | — | Async data loader. Stream rows in chunks, optionally tagged by source. |
114
+ | `onComplete` | `(result, actions) => void` | No | — | Called on submit. See [Submission Result](#submission-result). |
115
+ | `variant` | `"editor"` \| `"uploader"` | No | `"editor"` | Initial view. `"uploader"` opens the import wizard first. |
116
+ | `translations` | `DataEditorTranslations` | No | — | Partial i18n overrides. |
117
+ | `locale` | `string` | No | `"en"` | BCP 47 locale tag. |
118
+ | `rtl` | `boolean` | No | `false` | Right-to-left layout. |
119
+ | `readonly` | `boolean` | No | `false` | Hide all editing UI. |
120
+ | `enableDeleteRow` | `"all"` \| `"new"` \| `false` | No | `false` | Row deletion policy. |
121
+ | `enableAddRow` | `boolean` | No | `true` | Show the "Add row" button. |
122
+ | `enableCreateColumn` | `boolean` | No | `true` | Allow creating columns for unmatched CSV headers during import. |
123
+ | `importFormats` | `DataEditorFormat[]` | No | all | Allowed import formats. `[]` disables import. |
124
+ | `exportFormats` | `DataEditorFormat[]` | No | all | Allowed export formats. `[]` disables export. |
125
+ | `remoteSources` | `RemoteSource[]` | No | | Custom import buttons (Google Sheets, S3, etc.) rendered on the upload step. |
126
+ | `rowHeight` | `number` | No | `34` | Row height in pixels. |
127
+ | `headerHeight` | `number` | No | `36` | Header height in pixels. |
128
+ | `server` | `DataEditorServer<TRow>` | No | — | Server-delegated mode: SDK renders, your backend handles queries and mutations. |
129
+ | `chat` | `DataEditorChat<TRow>` | No | — | Bring-your-own AI chat panel. |
130
+ | `onColumnMatch` | `(headers, columns) => ...` | No | — | Override import column matching. |
131
+ | `onValueMatch` | `(valuesToMatch) => ...` | No | — | Override import value matching for `select` columns. |
132
+ | `synonyms` | `Record<string, string[]>` | No | — | Extra synonyms for column auto-matching. |
133
+ | `sampleData` | `Record<string, unknown>[]` | No | — | Rows used in the "Download Example" file. |
134
+ | `localStorage` | `false` \| `{ licenseGrant?: boolean }` | No | `{ licenseGrant: true }` | What the SDK caches in `localStorage`. |
135
+ | `onError` | `(error: UpdogError) => void` | No | — | Called on internal errors. Use for logging or Sentry. |
136
+ | `className` | `string` | No | — | CSS class on the wrapper element. |
96
137
 
97
138
  ## Column Configuration
98
139
 
99
140
  ```tsx
141
+ import { required, email } from "@updog/data-editor";
142
+ import type { DataEditorColumn } from "@updog/data-editor";
143
+
100
144
  const columns: DataEditorColumn[] = [
101
145
  {
102
146
  id: "email",
103
147
  title: "Email",
104
- width: 260,
148
+ size: 260,
105
149
 
106
- // Validation single or array of validators
107
- validate: (value, row) => {
108
- if (!value) return { level: "error", message: "Required" };
109
- if (!String(value).includes("@")) return { level: "warning", message: "Invalid email" };
110
- return null;
111
- },
150
+ // One validator, or an array. Each returns `{ level: "error", message }` or null.
151
+ validate: [required("Email is required"), email("Enter a valid email")],
112
152
 
113
- // Uniqueness constraint
153
+ // Flag duplicates in this column as errors.
114
154
  unique: true,
115
155
 
116
- // Cross-field validation — revalidate these columns when this one changes
156
+ // Revalidate these columns when this one changes.
117
157
  dependentFields: ["confirmEmail"],
118
158
 
119
- // Cell editor: "text" (default), "date", or "select"
120
- editor: { type: "select", options: [{ id: "us", text: "United States" }] },
159
+ // Cell editor. Default is "text".
160
+ editor: { type: "select", options: ["US", "UK", "DE"] },
121
161
 
122
- // Visual formatting (display only, does not change data)
162
+ // Display-only formatting. Does not mutate stored data.
123
163
  formatter: (value) => value.toLowerCase(),
124
164
 
125
- // Data transformation on input
165
+ // Runs when rows are uploaded to the editor. Mutates the stored value.
126
166
  transformer: (value) => String(value).trim(),
127
167
 
128
- // Filter panel configuration
168
+ // Sidebar filter control.
129
169
  filter: { type: "select" },
170
+
171
+ // Lock cells in this column. `"default"` locks only default-source rows.
172
+ locked: "default",
130
173
  },
131
174
  ];
132
175
  ```
133
176
 
177
+ **Cell editors**: `{ type: "text" }` (default), `{ type: "date", minDate?, maxDate? }`, `{ type: "select", options: string[] }`, `{ type: "number", decimalPlaces?, decimalSeparator?, thousandsSeparator?, allowChars? }`.
178
+
179
+ **Column filters**: `{ type: "select", label?, placeholder?, options?, multiple? }`, `{ type: "number-range", label? }`, `{ type: "date-range", label? }`.
180
+
134
181
  ## Built-in Validators
135
182
 
183
+ Validators are factory functions — call each one with the error message you want displayed.
184
+
136
185
  ```tsx
137
- import { useDataEditorValidators } from "@updog/data-editor";
138
-
139
- function App() {
140
- const { required, numeric, email, date, endDateAfterStart } = useDataEditorValidators();
141
-
142
- const columns = [
143
- { id: "email", title: "Email", validate: [required, email] },
144
- { id: "salary", title: "Salary", validate: numeric },
145
- { id: "startDate", title: "Start", validate: date, editor: { type: "date" } },
146
- {
147
- id: "endDate",
148
- title: "End",
149
- validate: [date, endDateAfterStart("startDate")],
150
- dependentFields: ["startDate"],
151
- editor: { type: "date" },
152
- },
153
- ];
154
- }
186
+ import {
187
+ required,
188
+ numeric,
189
+ email,
190
+ date,
191
+ oneOf,
192
+ endDateAfterStart,
193
+ } from "@updog/data-editor";
194
+
195
+ const columns = [
196
+ { id: "name", title: "Name", validate: required("Required") },
197
+ { id: "email", title: "Email", validate: [required("Required"), email("Invalid email")] },
198
+ { id: "salary", title: "Salary", validate: numeric("Must be a number") },
199
+ { id: "status", title: "Status", validate: oneOf(["active", "inactive"], "Invalid status") },
200
+ {
201
+ id: "startDate",
202
+ title: "Start",
203
+ validate: date("Use YYYY-MM-DD or DD/MM/YYYY"),
204
+ editor: { type: "date" },
205
+ dependentFields: ["endDate"],
206
+ },
207
+ {
208
+ id: "endDate",
209
+ title: "End",
210
+ validate: [date("Invalid date"), endDateAfterStart("startDate", "End must be on or after start")],
211
+ editor: { type: "date" },
212
+ },
213
+ ];
214
+ ```
215
+
216
+ A `ValidationError` with `level: "error"` flags the cell in the grid but does **not** block submission — invalid rows are delivered to `onComplete` tagged via `isValid: false`.
217
+
218
+ ### Custom validators
219
+
220
+ A validator is `(value, row) => ValidationError | null`. The only `level` is `"error"`.
221
+
222
+ ```tsx
223
+ import type { CellValidator } from "@updog/data-editor";
224
+
225
+ const mustContainAt: CellValidator = (value) => {
226
+ if (!value) return null;
227
+ if (!String(value).includes("@")) return { level: "error", message: "Must contain @" };
228
+ return null;
229
+ };
155
230
  ```
156
231
 
157
232
  ## Data Loading
158
233
 
159
- Data is loaded in chunks to avoid blocking the UI:
234
+ `loadData` is called once when the editor opens. Call `onChunk` one or more times to stream rows. The grid renders each chunk without blocking.
160
235
 
161
236
  ```tsx
162
- const loadData = async (onChunk) => {
163
- // Load from API in pages
237
+ // Single source
238
+ loadData={async (onChunk) => {
164
239
  for (let page = 0; page < totalPages; page++) {
165
- const res = await fetch(`/api/employees?page=${page}`);
166
- const rows = await res.json();
167
- onChunk(rows); // Each chunk is immediately rendered
240
+ const rows = await fetch(`/api/employees?page=${page}`).then((r) => r.json());
241
+ onChunk(rows);
168
242
  }
169
- };
243
+ }}
244
+
245
+ // Multiple sources — each chunk tagged with a source
246
+ loadData={async (onChunk) => {
247
+ onChunk(await fetchSalesforce(), { source: "Salesforce", done: true });
248
+ onChunk(await fetchHubSpot(), { source: "HubSpot", deletable: true, done: true });
249
+ }}
170
250
  ```
171
251
 
252
+ `ChunkSourceOptions`: `{ source: string; id?: string; deletable?: boolean; done?: boolean }`. Sources are auto-registered on first encounter. Chunks without options go to "Existing Data". When `loadData` resolves, any source still loading is finalized automatically.
253
+
172
254
  ## Submission Result
173
255
 
174
- ```tsx
175
- const onComplete = (result, actions) => {
176
- // result.valid — rows that pass all validation
177
- // result.invalid — rows with validation errors
178
- // result.deleted — rows marked for deletion
179
- // result.newCount, result.editedCount, result.deletedCount
256
+ `onComplete(result, actions)` fires when the user submits.
257
+
258
+ ```ts
259
+ type DataEditorResult<TRow> = {
260
+ sources: {
261
+ sourceId: string;
262
+ sourceName: string;
263
+ rows: {
264
+ row: TRow;
265
+ isNew: boolean; // imported or manually added this session
266
+ isChanged: boolean; // cell values differ from origin
267
+ isDeleted: boolean; // marked for deletion
268
+ isValid: boolean; // passes all validators
269
+ }[];
270
+ }[];
271
+ counts: { new: number; changed: number; deleted: number; invalid: number };
272
+ };
180
273
 
181
- // actions.reset() clear all changes and reload
274
+ type DataEditorActions = {
275
+ reset: () => void; // discard changes and reload via loadData
182
276
  };
183
277
  ```
184
278
 
185
- ## Import/Export Formats
279
+ Pristine backend rows (unchanged, not deleted, not new) are omitted from `sources[].rows` — they're no-ops. The flags are orthogonal: a row can be new, changed, and deleted at once. You own the routing rules:
186
280
 
187
- Supported formats: `"csv"`, `"tsv"`, `"xlsx"`, `"json"`, `"xml"`.
281
+ ```tsx
282
+ onComplete={async (result, actions) => {
283
+ for (const source of result.sources) {
284
+ const inserts = source.rows.filter((r) => r.isNew && !r.isDeleted && r.isValid);
285
+ const updates = source.rows.filter((r) => !r.isNew && r.isChanged && !r.isDeleted && r.isValid);
286
+ const deletes = source.rows.filter((r) => r.isDeleted && !r.isNew);
287
+ await persist(source.sourceId, { inserts, updates, deletes });
288
+ }
289
+ actions.reset();
290
+ }}
291
+ ```
292
+
293
+ If you never tagged sources via `loadData`, you'll get one entry with `sourceId: "backend"`. `result.counts` is an aggregate for summary UI — route with the per-row flags, not counts.
294
+
295
+ ## Import Configuration
296
+
297
+ The import wizard guides users through uploading a file, mapping columns to your schema, and resolving value mismatches.
298
+
299
+ ### Formats
300
+
301
+ Supported: `"csv"`, `"tsv"`, `"xlsx"`, `"json"`, `"xml"`.
188
302
 
189
303
  ```tsx
190
- // Only allow CSV and XLSX import, disable export
191
304
  <DataEditor
192
- importFormats={["csv", "xlsx"]}
193
- exportFormats={[]}
305
+ importFormats={["csv", "xlsx"]} // only CSV and XLSX
306
+ exportFormats={[]} // disable export entirely
194
307
  />
195
308
  ```
196
309
 
310
+ ### Override column matching
311
+
312
+ ```tsx
313
+ onColumnMatch={async (headers, columns) => {
314
+ const mappings = await myMatchingService.match(headers, columns);
315
+ return mappings; // { csvHeader: columnId | null }
316
+ }}
317
+ ```
318
+
319
+ Return a map of `{ csvHeader: columnId | null }`. Entries set to `null` or omitted fall back to built-in matching.
320
+
321
+ ### Override value matching
322
+
323
+ For select columns, override imported-value → option mapping:
324
+
325
+ ```tsx
326
+ onValueMatch={async (valuesToMatch) => {
327
+ // valuesToMatch = { country: { importedValues: ["espana", "fr"], options: ["Spain", "France"] } }
328
+ return {
329
+ country: { espana: "Spain", fr: "France" },
330
+ };
331
+ }}
332
+ ```
333
+
334
+ Values set to `null` skip auto-matching. Unmapped values fall back to built-in fuzzy matching.
335
+
336
+ ### Synonyms
337
+
338
+ Extra aliases for the built-in column matcher:
339
+
340
+ ```tsx
341
+ synonyms={{
342
+ productSku: ["sku", "article_no", "item_code"],
343
+ firstName: ["first", "given_name", "fname"],
344
+ }}
345
+ ```
346
+
347
+ ### Remote sources
348
+
349
+ Render custom import buttons for third-party sources. You own auth and picker logic; the SDK renders the button and processes the result.
350
+
351
+ ```tsx
352
+ const googleSheets: RemoteSource = {
353
+ id: "google-sheets",
354
+ label: "Google Sheets",
355
+ icon: "<svg>...</svg>",
356
+ fetch: async () => {
357
+ const data = await myGoogleSheetsLib.pick();
358
+ return data.rows; // Record<string, unknown>[] — or return a File
359
+ },
360
+ };
361
+
362
+ <DataEditor remoteSources={[googleSheets]} {...rest} />
363
+ ```
364
+
365
+ Return a `File` to parse via the standard CSV/XLSX/JSON/XML pipeline, or return structured records to skip parsing.
366
+
367
+ ## Error Handling
368
+
369
+ Log SDK-internal errors through `onError`. The SDK recovers gracefully — use this for monitoring, not recovery.
370
+
371
+ ```tsx
372
+ onError={(error) => {
373
+ Sentry.captureException(error.originalError ?? error, {
374
+ tags: { code: error.code, source: error.source },
375
+ });
376
+ }}
377
+ ```
378
+
379
+ Error codes: `PARSE_ERROR`, `RENDER_ERROR`, `TRANSFORM_ERROR`, `VALIDATION_ERROR`, `WORKER_ERROR`, `COMMAND_ERROR`, `OPERATION_ERROR`.
380
+
197
381
  ## Non-React Usage
198
382
 
199
- For Angular, Vue, or vanilla JS, use the Web Component package:
383
+ For Angular, Vue, Svelte, or vanilla JS use the Web Component build:
200
384
 
201
385
  ```bash
202
386
  npm install @updog/data-editor-wc
203
387
  ```
204
388
 
205
- See [@updog/data-editor-wc](https://www.npmjs.com/package/@updog/data-editor-wc) for details.
206
-
207
- ## Key Features
208
-
209
- - Virtualized grid — smooth scrolling with 100k+ rows
210
- - Multi-source data — backend data + multiple CSV/XLSX imports merged into one view
211
- - Smart CSV column matching — auto-maps imported columns with 90%+ accuracy
212
- - Undo/redo — Cmd+Z / Cmd+Shift+Z with batch support
213
- - Find & replace — search across all columns with match highlighting
214
- - Cell validation — custom validators, uniqueness constraints, cross-field dependencies
215
- - Dirty tracking — distinguishes new vs edited rows, detects value reverts
389
+ See [@updog/data-editor-wc](https://www.npmjs.com/package/@updog/data-editor-wc).
216
390
 
217
391
  ## License
218
392
 
219
- Commercial — see [LICENSE](./LICENSE) for the full terms of the Updog SDK Commercial License v1.0. Contact `admin@updog.tech` for enterprise licensing questions. Third-party dependencies bundled with this package are listed in `THIRD_PARTY_NOTICES.txt`.
393
+ Commercial — see [LICENSE](./LICENSE) for the full terms of the Updog SDK Commercial License v1.0. Contact `admin@updog.tech` for enterprise licensing. Third-party dependencies are listed in `THIRD_PARTY_NOTICES.txt`.