@vuu-ui/vuu-data-editing 3.5.4 → 3.5.6

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 CHANGED
@@ -2,4 +2,367 @@
2
2
 
3
3
  Shared data editing APIs, state management, and React components for VUU UI.
4
4
 
5
- See [DESIGN.md](./DESIGN.md) for the package design and file responsibilities.
5
+ This package coordinates edit-session state with editable data sources (both in-memory and remote Vuu session tables) and provides React hooks and UI controls for table editing.
6
+
7
+ ---
8
+
9
+ ## Table of Contents
10
+
11
+ - [Overview & Architecture](#overview--architecture)
12
+ - [EditSession](#editsession)
13
+ - [Lifecycle State Machine](#lifecycle-state-machine)
14
+ - [Transition Queue](#transition-queue)
15
+ - [EditState & Counters](#editstate--counters)
16
+ - [Internal Edit Tracking](#internal-edit-tracking)
17
+ - [Editing Operations & Flows](#editing-operations--flows)
18
+ - [Cell Editing (`commit` / `cancel`)](#cell-editing-commit--cancel)
19
+ - [New Row Flow (`addNewRow` / `addRow`)](#new-row-flow-addnewrow--addrow)
20
+ - [Row Deletions (`deleteSelectedRows`)](#row-deletions-deleteselectedrows)
21
+ - [Undo Flow (`undoRowChange`)](#undo-flow-undorowchange)
22
+ - [Session Completion (`end`) & Stale Handling](#session-completion-end--stale-handling)
23
+ - [Direct Editing (`DirectEditSession`)](#direct-editing-directeditsession)
24
+ - [React Hooks & Context](#react-hooks--context)
25
+ - [`useEditableTable`](#useeditabletable)
26
+ - [`DataEditingProvider` & `useEditSession`](#dataeditingprovider--useeditsession)
27
+ - [`useCellEdited`](#usecellEdited)
28
+ - [`useEditMode` & `EditModeProvider`](#useeditmode--editmodeprovider)
29
+ - [UI Components & Helpers](#ui-components--helpers)
30
+ - [`EditButtons`](#editbuttons)
31
+ - [`UndoCellRenderer`](#undocellrenderer)
32
+ - [`editActionRowClassNameGenerator`](#editactionrowclassnamegenerator)
33
+ - [`isRowSelectable` / `isEditRowReadOnly`](#isrowselectable--iseditrowreadonly)
34
+ - [Standard Integration Pattern](#standard-integration-pattern)
35
+
36
+ ---
37
+
38
+ ## Overview & Architecture
39
+
40
+ VUU editing operates primarily through **staged edit sessions**:
41
+ 1. When entering edit mode, a server-side (or mock) **session table** is created as a sandbox copy of the source table.
42
+ 2. User edits (cell edits, row insertions, soft deletes) are applied to the session table and tracked locally by `EditSession`.
43
+ 3. When saved, changes are committed from the session table to the source table. When cancelled, the session table is discarded without affecting the source table.
44
+
45
+ ```
46
+ Source DataSource ──begin()──> Session Table Created ──> Live Session DataSource
47
+ │ │
48
+ │ Edits & Deletes
49
+ │ │
50
+ └──────────────end(save=true/false)──────────────────────┘
51
+ ```
52
+
53
+ For backends that do not use session tables and accept cell edits directly against the live table, `DirectEditSession` is used instead.
54
+
55
+ ---
56
+
57
+ ## EditSession
58
+
59
+ `EditSession` is the core stateful orchestrator class extending `EventEmitter`. It tracks:
60
+ - Local cell edits per row and column
61
+ - Added row keys
62
+ - Edit, invalid, delete, and add counts
63
+ - Edit session lifecycle state
64
+
65
+ ### Lifecycle State Machine
66
+
67
+ `EditSession.lifecycle` is a discriminated union on `status`:
68
+
69
+ ```typescript
70
+ type EditLifecycle =
71
+ | { status: "idle" }
72
+ | { status: "starting" }
73
+ | { status: "active"; sessionDataSource: DataSource }
74
+ | { status: "ending"; sessionDataSource: DataSource }
75
+ | { status: "error"; operation: "begin" | "end"; error: Error; sessionDataSource?: DataSource };
76
+ ```
77
+
78
+ ```
79
+ idle ──begin()──> starting ──> active ──end()──> ending ──> idle
80
+ │ │
81
+ └── error { op: "begin" } └── error { op: "end" }
82
+ ```
83
+
84
+ - `inEditMode`: Getter returning `true` when status is `"active"`, `"ending"`, or `error { operation: "end" }`.
85
+ - `dataSource`: Getter returning `#sessionDataSource` when active/ending, falling back to `#sourceTableDataSource`.
86
+
87
+ ### Transition Queue
88
+
89
+ All `begin()` and `end()` calls are serialized through `#transitionQueue` (a chained Promise). This prevents overlapping transitions, race conditions, and corrupted lifecycle states when rapid toggles occur.
90
+
91
+ ### EditState & Counters
92
+
93
+ `editSession.editState` is a derived property reflecting the overall dirtiness and validity of the session:
94
+
95
+ ```typescript
96
+ type EditState = "clean" | "dirty" | "invalid" | "stale";
97
+ ```
98
+
99
+ Priority order (highest to lowest):
100
+ 1. **`"invalid"`**: When `invalidCount > 0` (validation failure in at least one cell).
101
+ 2. **`"stale"`**: When a save was rejected by the server due to concurrent updates (`StaleUpdateError`).
102
+ 3. **`"dirty"`**: When `editCount > 0 || deleteCount > 0 || addCount > 0`.
103
+ 4. **`"clean"`**: When no edits, deletes, or added rows exist.
104
+
105
+ Driven by four independent counters:
106
+ - `editCount`: Number of valid cell edits differing from their original values.
107
+ - `invalidCount`: Number of cell edits with validation errors.
108
+ - `deleteCount`: Number of rows marked for deletion.
109
+ - `addCount`: Number of newly inserted rows currently in the session.
110
+
111
+ All counter setters enforce non-negative values via `Math.max(0, val)`.
112
+
113
+ ### Internal Edit Tracking
114
+
115
+ `EditSession` manages four primary collections:
116
+
117
+ ```typescript
118
+ #rowEdits = new Map<string, RowEditDetails>(); // rowKey => { cellEdits: Map<column, CellEdit> }
119
+ #addedRowKeys = new Set<string>(); // Set of added row keys
120
+ #cellCommitRevisions = new Map<string, Map<string, number>>(); // (key, col) => revision
121
+ #transitionQueue = Promise.resolve(); // Serialized transition queue
122
+ ```
123
+
124
+ `CellEdit` holds:
125
+ ```typescript
126
+ type CellEdit = {
127
+ originalValue: EditSessionValue;
128
+ editedValue: EditSessionValue;
129
+ isValid: boolean;
130
+ };
131
+ ```
132
+ A cell is considered edited only when `originalValue !== editedValue && isValid === true`. If edited back to the original value, the entry is automatically pruned.
133
+
134
+ ---
135
+
136
+ ## Editing Operations & Flows
137
+
138
+ ### Cell Editing (`commit` / `cancel`)
139
+
140
+ - **`commit(key, columnName, originalValue, typedValue, isValid, options)`**:
141
+ - Increments cell revision to guard against out-of-order RPC responses.
142
+ - Updates `#rowEdits`, adjusting `editCount` and `invalidCount`.
143
+ - Dispatches `editCell` RPC to the active DataSource if `syncToDataSource !== false`.
144
+ - Emits `"cellEditChanged"` and `"editState"`.
145
+ - **`cancel(key, columnName, restoredValue)`**:
146
+ - Discards an in-progress invalid edit, restoring the cell to its original valid state. Uncommitted typed values are discarded client-side without sending an RPC.
147
+
148
+ ### New Row Flow (`addNewRow` / `addRow`)
149
+
150
+ 1. **Configure**: `editSession.configureNewRow(columns, requiredColumns)` defines the expected column inputs.
151
+ 2. **Draft**: `editSession.setNewRowValue(column, value)` updates `#newRowState.values`.
152
+ 3. **Submit**: `editSession.addNewRow()` validates required columns and delegates to `addRow()`.
153
+ 4. **`addRow(rowData)`**:
154
+ - Calls `dataSource.addRow(rowData)` with `#rowDefaults` merged in.
155
+ - On success, extracts the row key directly from the response map (`response.data.key`) and records it in `#addedRowKeys`.
156
+ - Increments `addCount`.
157
+ - Resets new-row form draft and increments `draftRevision`.
158
+
159
+ ### Row Deletions (`deleteSelectedRows`)
160
+
161
+ - Captures `selectedRowsCount` before deleting.
162
+ - Exits early if `selectedRowsCount === 0`.
163
+ - Calls `dataSource.deleteSelectedRows(deleteMode)`.
164
+ - On success:
165
+ - Increments `deleteCount` by `selectedRowsCount`.
166
+ - **Clears selection**: Dispatches `dataSource.select({ type: "DESELECT_ALL" })` so deleted rows are immediately deselected, preventing accidental double-deletion and disabling the Delete button.
167
+
168
+ ### Undo Flow (`undoRowChange`)
169
+
170
+ Invoked when the user clicks the Undo button on a row (`UndoCellRenderer`):
171
+
172
+ ```typescript
173
+ editSession.undoRowChange(key, action);
174
+ ```
175
+
176
+ 1. **Reverts cell edits**: Clears recorded cell edits for `key`, restores previous values, and adjusts `editCount` / `invalidCount`.
177
+ 2. **Reverts deletions**: If `action === "deleteRow"`, decrements `deleteCount`.
178
+ 3. **Reverts insertions**: If the row was an inserted row:
179
+ - `action === "addRow"` OR
180
+ - Row key exists in private `#addedRowKeys`
181
+ - Decrements `addCount` and removes `key` from `#addedRowKeys`.
182
+
183
+ > **Robust Add-Delete-Undo**: Even if a row was added, then soft-deleted (`action` became `"deleteRow"`), and the server purges the row on undo without returning any special flag, `EditSession` identifies the row via `#addedRowKeys` and decrements **both** `deleteCount` and `addCount`, returning `editState` cleanly to `"clean"`.
184
+
185
+ ### Session Completion (`end`) & Stale Handling
186
+
187
+ - **`end(saveChanges = false, force = false)`**:
188
+ - Transitions lifecycle to `"ending"`.
189
+ - Calls `dataSource.endEditSession(saveChanges, force)`.
190
+ - On success: clears all edits, resets counters, unsubscribes session DataSource, transitions to `"idle"`.
191
+ - **Stale Updates (`StaleUpdateError`)**: If the server rejects the commit due to concurrent edits, lifecycle transitions to `error { operation: "end" }` and `editState` becomes `"stale"`. The user can force-save with `end(true, true)`.
192
+
193
+ ---
194
+
195
+ ## Direct Editing (`DirectEditSession`)
196
+
197
+ Implements `TableEditSession` for tables that support in-place cell editing without a session table or lifecycle state machine:
198
+ - Does not create session tables or track `editCount`.
199
+ - Each `commit()` forwards directly to `dataSource.editCell()`.
200
+ - `inEditMode` is always `false`.
201
+
202
+ ---
203
+
204
+ ## React Hooks & Context
205
+
206
+ ### `useEditableTable`
207
+
208
+ The primary orchestrator hook for table components:
209
+
210
+ ```typescript
211
+ const {
212
+ canCancel, // boolean: safe to show/enable Cancel button
213
+ canSave, // boolean: active, dirty/stale, invalidCount === 0
214
+ dataSource, // Active DataSource (session DS in edit mode, else source DS)
215
+ sourceDataSource, // Always the source DataSource
216
+ sessionDataSource, // Session DataSource (when active)
217
+ editSession, // EditSession instance
218
+ lifecycle, // Current EditLifecycle
219
+ hasSelection, // boolean: whether rows are selected (controls Delete button)
220
+ isEditMode, // boolean: current edit mode flag
221
+ isEditSessionReady, // boolean: gates rendering the editable Table
222
+ isRowSelectable, // (dataRow) => boolean: blocks selection of soft-deleted rows
223
+ rowClassNameGenerators, // [vuu-edit-actions] in edit mode, else undefined
224
+ onCancel, // async handler: ends session and calls props.onCancel
225
+ onDelete, // async handler: deletes selected rows
226
+ onSave, // async handler: ends session (with optional force) and calls props.onSave
227
+ onUndoRowChange, // (key, action) => void
228
+ } = useEditableTable(props);
229
+ ```
230
+
231
+ ### `DataEditingProvider` & `useEditSession`
232
+
233
+ Distributes `EditSession` through React Context to descendant cells and controls:
234
+
235
+ ```tsx
236
+ <DataEditingProvider editSession={editSession}>
237
+ {/* Table and EditButtons subtree */}
238
+ </DataEditingProvider>
239
+ ```
240
+
241
+ - `useEditSession()`: Returns `EditSession | undefined`.
242
+ - `useEditSession(true)`: Returns `EditSession`, throws if unmounted.
243
+ - `useTableEditSession()`: Returns generic `TableEditSession` (works with both `EditSession` and `DirectEditSession`).
244
+
245
+ ### `useCellEdited`
246
+
247
+ Uses `useSyncExternalStore` for zero-lag cell edit detection. Returns `true` if `(rowKey, columnName)` has an uncommitted edit:
248
+
249
+ ```typescript
250
+ const isEdited = useCellEdited(editSession, rowKey, columnName);
251
+ ```
252
+
253
+ ### `useEditMode` & `EditModeProvider`
254
+
255
+ Optional context for coordinating a boolean edit/view mode toggle across independent toolbar and table components:
256
+
257
+ ```tsx
258
+ const { isEditMode, setEditMode } = useEditMode();
259
+ ```
260
+
261
+ ---
262
+
263
+ ## UI Components & Helpers
264
+
265
+ ### `EditButtons`
266
+
267
+ Pre-built action buttons wired to `EditSession`:
268
+ - **Delete**: Enabled only when `hasSelection === true`.
269
+ - **Save**: Enabled when `canSave === true`. Displays `"Save (force)"` when `editState === "stale"`.
270
+ - **Cancel**: Enabled when `canCancel === true`.
271
+
272
+ ```tsx
273
+ <EditButtons
274
+ canCancel={canCancel}
275
+ canSave={canSave}
276
+ editSession={editSession}
277
+ hasSelection={hasSelection}
278
+ onCancel={onCancel}
279
+ onDelete={onDelete}
280
+ onSave={onSave}
281
+ />
282
+ ```
283
+
284
+ ### `UndoCellRenderer`
285
+
286
+ Registered as `"vuu.undo-cell"`. Renders an Undo button in the row's action column:
287
+ - Dispatches `editSession.undoRowChange(key, action)` on click.
288
+ - Tooltip dynamically reflects action: `"Undo insert row"`, `"Undo delete row"`, or `"Undo row edits"`.
289
+
290
+ ### `editActionRowClassNameGenerator`
291
+
292
+ Registered as `"vuu-edit-actions"`. Applies row CSS classes based on `dataRow.vuuAction`:
293
+ - `"addRow"` → `.vuuTableRow-inserted`
294
+ - `"deleteRow"` → `.vuuTableRow-deleted`
295
+
296
+ ### `isRowSelectable` / `isEditRowReadOnly`
297
+
298
+ - `isEditRowReadOnly(dataRow)`: Returns `true` if `dataRow.vuuAction === "deleteRow"`.
299
+ - `isRowSelectable`: Exported from `useEditableTable` as `(dataRow) => !isEditRowReadOnly(dataRow)`. Pass to `<Table isRowSelectable={isRowSelectable} />` to prevent users from selecting soft-deleted rows.
300
+
301
+ ---
302
+
303
+ ## Standard Integration Pattern
304
+
305
+ ```tsx
306
+ import React, { useCallback, useState } from "react";
307
+ import { Table } from "@vuu-ui/vuu-table";
308
+ import {
309
+ DataEditingProvider,
310
+ EditButtons,
311
+ useEditableTable,
312
+ } from "@vuu-ui/vuu-data-editing";
313
+
314
+ export const EditableTableFeature = ({ sourceDataSource, tableConfig }) => {
315
+ const [isEditMode, setIsEditMode] = useState(false);
316
+
317
+ const {
318
+ canCancel,
319
+ canSave,
320
+ dataSource,
321
+ editSession,
322
+ hasSelection,
323
+ isEditSessionReady,
324
+ isRowSelectable,
325
+ onCancel,
326
+ onDelete,
327
+ onSave,
328
+ rowClassNameGenerators,
329
+ } = useEditableTable({
330
+ dataSource: sourceDataSource,
331
+ isEditMode,
332
+ onCancel: () => setIsEditMode(false),
333
+ onSave: () => setIsEditMode(false),
334
+ });
335
+
336
+ return (
337
+ <DataEditingProvider editSession={editSession}>
338
+ <div className="table-container">
339
+ {(!isEditMode || isEditSessionReady) && (
340
+ <Table
341
+ config={tableConfig}
342
+ dataSource={dataSource}
343
+ isRowSelectable={isRowSelectable}
344
+ rowClassNameGenerators={rowClassNameGenerators}
345
+ />
346
+ )}
347
+ </div>
348
+
349
+ <div className="toolbar">
350
+ {!isEditMode ? (
351
+ <button onClick={() => setIsEditMode(true)}>Edit</button>
352
+ ) : (
353
+ <EditButtons
354
+ canCancel={canCancel}
355
+ canSave={canSave}
356
+ editSession={editSession}
357
+ hasSelection={hasSelection}
358
+ onCancel={onCancel}
359
+ onDelete={onDelete}
360
+ onSave={onSave}
361
+ />
362
+ )}
363
+ </div>
364
+ </DataEditingProvider>
365
+ );
366
+ };
367
+ ```
368
+
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "3.5.4",
2
+ "version": "3.5.6",
3
3
  "description": "Shared data editing APIs and React components for VUU UI",
4
4
  "main": "./src/index.js",
5
5
  "author": "heswell",
@@ -12,13 +12,13 @@
12
12
  },
13
13
  "dependencies": {
14
14
  "@salt-ds/core": "1.54.1",
15
- "@vuu-ui/vuu-ui-controls": "3.5.4",
16
- "@vuu-ui/vuu-utils": "3.5.4"
15
+ "@vuu-ui/vuu-ui-controls": "3.5.6",
16
+ "@vuu-ui/vuu-utils": "3.5.6"
17
17
  },
18
18
  "devDependencies": {
19
- "@vuu-ui/vuu-data-types": "3.5.4",
20
- "@vuu-ui/vuu-protocol-types": "3.5.4",
21
- "@vuu-ui/vuu-table-types": "3.5.4"
19
+ "@vuu-ui/vuu-data-types": "3.5.6",
20
+ "@vuu-ui/vuu-protocol-types": "3.5.6",
21
+ "@vuu-ui/vuu-table-types": "3.5.6"
22
22
  },
23
23
  "peerDependencies": {
24
24
  "react": "^19.2.3"
@@ -16,6 +16,7 @@ class SupersededEditError extends Error {
16
16
  class EditSession extends EventEmitter {
17
17
  static newRowKey = "__vuu_new_row__";
18
18
  #rowEdits = new Map();
19
+ #addedRowKeys = new Set();
19
20
  #editCount = 0;
20
21
  #deleteCount = 0;
21
22
  #addCount = 0;
@@ -88,19 +89,21 @@ class EditSession extends EventEmitter {
88
89
  }
89
90
  }
90
91
  #setDeleteCount(val) {
91
- if (val !== this.#deleteCount) {
92
+ const newCount = Math.max(0, val);
93
+ if (newCount !== this.#deleteCount) {
92
94
  const oldState = this.editState;
93
95
  const oldCount = this.#deleteCount;
94
- this.#deleteCount = val;
95
- this.#emitEditStateChange(oldState, 0 === oldCount || 0 === val);
96
+ this.#deleteCount = newCount;
97
+ this.#emitEditStateChange(oldState, 0 === oldCount || 0 === newCount);
96
98
  }
97
99
  }
98
100
  #setAddCount(val) {
99
- if (val !== this.#addCount) {
101
+ const newCount = Math.max(0, val);
102
+ if (newCount !== this.#addCount) {
100
103
  const oldState = this.editState;
101
104
  const oldCount = this.#addCount;
102
- this.#addCount = val;
103
- this.#emitEditStateChange(oldState, 0 === oldCount || 0 === val);
105
+ this.#addCount = newCount;
106
+ this.#emitEditStateChange(oldState, 0 === oldCount || 0 === newCount);
104
107
  }
105
108
  }
106
109
  #setStale(isStale) {
@@ -234,10 +237,17 @@ class EditSession extends EventEmitter {
234
237
  const deleteSelectedRows = this.dataSource?.deleteSelectedRows;
235
238
  if (void 0 === deleteSelectedRows) throw Error("[EditSession] datasource does not support deleting rows");
236
239
  const selectedRowsCount = this.dataSource?.selectedRowsCount ?? 0;
240
+ if (0 === selectedRowsCount) return {
241
+ data: void 0,
242
+ type: "SUCCESS_RESULT"
243
+ };
237
244
  const response = await deleteSelectedRows.call(this.dataSource, this.#deleteMode);
238
245
  if (void 0 === response) throw Error("[EditSession] datasource returned no response when deleting rows");
239
246
  if (isRpcError(response)) return response;
240
247
  if (selectedRowsCount > 0) this.#setDeleteCount(this.#deleteCount + selectedRowsCount);
248
+ await this.dataSource?.select?.({
249
+ type: "DESELECT_ALL"
250
+ });
241
251
  return response;
242
252
  }
243
253
  async addRow(rowData = {}) {
@@ -248,7 +258,11 @@ class EditSession extends EventEmitter {
248
258
  ...rowData
249
259
  });
250
260
  if (void 0 === response) throw Error("[EditSession] datasource returned no response when adding row");
251
- if (!isRpcError(response)) this.#setAddCount(this.#addCount + 1);
261
+ if (!isRpcError(response)) {
262
+ const key = response.data?.key;
263
+ if (null != key) this.#addedRowKeys.add(String(key));
264
+ this.#setAddCount(this.#addCount + 1);
265
+ }
252
266
  return response;
253
267
  }
254
268
  hasRowChanges(key) {
@@ -287,9 +301,12 @@ class EditSession extends EventEmitter {
287
301
  this.#invalidCount -= removedInvalidCount;
288
302
  for (const columnName of changedColumns)this.emit("cellEditChanged", key, columnName);
289
303
  }
290
- if ("deleteRow" === action) this.#deleteCount--;
291
- const wasInsertedRow = "addRow" === action || response?.data?.wasInsertedRow === true;
292
- if (wasInsertedRow) this.#addCount--;
304
+ if ("deleteRow" === action) this.#deleteCount = Math.max(0, this.#deleteCount - 1);
305
+ const wasInsertedRow = "addRow" === action || this.#addedRowKeys.has(key);
306
+ if (wasInsertedRow) {
307
+ this.#addedRowKeys.delete(key);
308
+ this.#addCount = Math.max(0, this.#addCount - 1);
309
+ }
293
310
  this.#emitEditStateChange(oldState);
294
311
  return response;
295
312
  }
@@ -304,6 +321,7 @@ class EditSession extends EventEmitter {
304
321
  columnName
305
322
  ]));
306
323
  this.#rowEdits.clear();
324
+ this.#addedRowKeys.clear();
307
325
  this.#cellCommitRevisions.clear();
308
326
  this.#editCount = 0;
309
327
  this.#deleteCount = 0;
@@ -2,6 +2,7 @@ import { useData, useLayoutEffectSkipFirst } from "@vuu-ui/vuu-utils";
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
3
  import { EditSession } from "./EditSession.js";
4
4
  import { EDIT_ACTION_ROW_CLASS_NAME_GENERATOR } from "./editActionRowClassNameGenerator.js";
5
+ import { isEditRowReadOnly } from "./edit-utils.js";
5
6
  const EDIT_ACTION_ROW_CLASS_NAME_GENERATORS = [
6
7
  EDIT_ACTION_ROW_CLASS_NAME_GENERATOR
7
8
  ];
@@ -148,6 +149,7 @@ const useEditableTable = ({ columns, dataSource: dataSourceProp, deleteMode = "s
148
149
  onDelete: handleDelete,
149
150
  onSave: handleSave,
150
151
  onUndoRowChange: handleUndoRowChange,
152
+ isRowSelectable: isEditMode ? (dataRow)=>!isEditRowReadOnly(dataRow) : void 0,
151
153
  rowClassNameGenerators: isEditMode ? EDIT_ACTION_ROW_CLASS_NAME_GENERATORS : void 0,
152
154
  sessionDataSource,
153
155
  sourceDataSource
@@ -21,6 +21,9 @@ export type EditSessionConstructorProps = {
21
21
  /** Default column values merged into every addRow call for absent columns. Pass a stable reference. */
22
22
  rowDefaults?: RowDefaultDataItemValues;
23
23
  };
24
+ export type AddRowResultData = {
25
+ key?: string | number;
26
+ };
24
27
  export type NewRowState = {
25
28
  columns: readonly string[];
26
29
  requiredColumns?: readonly string[];
package/types/index.d.ts CHANGED
@@ -8,7 +8,7 @@ export { EditButtons, type EditButtonProps } from "./EditButtons";
8
8
  export { EditField, type DropdownEditFieldProps, type EditFieldProps, type EditFieldType, type TextFieldType, } from "./edit-field/EditField";
9
9
  export { useEditField, type EditFieldHookProps, } from "./edit-field/useEditField";
10
10
  export { EditModeProvider, useEditMode, type EditModeContextProps, } from "./EditModeProvider";
11
- export { EditError, EditSession, SupersededEditError, type EditActionType, type EditCommitOptions, type EditLifecycle, type EditSessionConstructorProps, type EditSessionValue, type NewRowState, type EditState, type RowDefaultDataItemValues, } from "./EditSession";
11
+ export { EditError, EditSession, SupersededEditError, type AddRowResultData, type EditActionType, type EditCommitOptions, type EditLifecycle, type EditSessionConstructorProps, type EditSessionValue, type NewRowState, type EditState, type RowDefaultDataItemValues, } from "./EditSession";
12
12
  export { StaleUpdateError } from "@vuu-ui/vuu-utils";
13
13
  export { lookupOptionsFromRows, useLookupValues, type LookupOption, type LookupValuedHookProps, type OptionMap, } from "./lookup-values/useLookupValues";
14
14
  export { useEditable, type EditableHookProps } from "./useEditable";
@@ -1,5 +1,6 @@
1
1
  import type { CopyOption, DataSource, DeleteRowMode } from "@vuu-ui/vuu-data-types";
2
2
  import type { VuuTable } from "@vuu-ui/vuu-protocol-types";
3
+ import type { DataRow } from "@vuu-ui/vuu-table-types";
3
4
  import { EditSession, type EditActionType, type EditLifecycle, type EditSessionApi, type RowDefaultDataItemValues } from "./EditSession";
4
5
  export type EditMode = "edit" | "view";
5
6
  export interface EditableTableHookProps {
@@ -38,6 +39,7 @@ export declare const useEditableTable: ({ columns, dataSource: dataSourceProp, d
38
39
  onDelete: () => Promise<void>;
39
40
  onSave: (force?: boolean) => Promise<void>;
40
41
  onUndoRowChange: (key: string, action: EditActionType) => undefined;
42
+ isRowSelectable: ((dataRow: DataRow) => boolean) | undefined;
41
43
  rowClassNameGenerators: string[] | undefined;
42
44
  sessionDataSource: import("@vuu-ui/vuu-data-types").DataSourceBase<import("@vuu-ui/vuu-data-types").DataSourceRowWithBigint> | undefined;
43
45
  sourceDataSource: DataSource;
@@ -1,8 +0,0 @@
1
- import { type ReactNode } from "react";
2
- import type { EditSession } from "./EditSession";
3
- export declare const DataEditingProvider: ({ children, editSession, }: {
4
- children: ReactNode;
5
- editSession: EditSession;
6
- }) => import("react").JSX.Element;
7
- export declare function useEditSession(throwIfUnavailable?: false): EditSession | undefined;
8
- export declare function useEditSession(throwIfUnavailable: true): EditSession;
@@ -1,14 +0,0 @@
1
- import type { EditSession } from "./EditSession";
2
- export interface EditButtonProps {
3
- canCancel: boolean;
4
- canSave: boolean;
5
- editSession?: EditSession;
6
- hasSelection?: boolean;
7
- onCancel?: () => void;
8
- onDelete?: () => void;
9
- onSave: (force?: boolean) => void;
10
- saveLabel?: string;
11
- confirmSave?: () => boolean | Promise<boolean>;
12
- confirmCancel?: () => boolean | Promise<boolean>;
13
- }
14
- export declare const EditButtons: ({ canCancel, canSave, confirmCancel, confirmSave, editSession, hasSelection, onCancel, onDelete, onSave, saveLabel, }: EditButtonProps) => import("react").JSX.Element;
@@ -1,13 +0,0 @@
1
- import { ReactNode } from "react";
2
- export interface EditModeContextProps {
3
- isEditMode: boolean;
4
- setEditMode: (inEditMode: boolean) => void;
5
- }
6
- /**
7
- * Implemented as a standalone Provider so that EditMode cna be implemented
8
- * at higher level than individual edit controls.
9
- */
10
- export declare const EditModeProvider: ({ children }: {
11
- children: ReactNode;
12
- }) => import("react").JSX.Element;
13
- export declare const useEditMode: () => EditModeContextProps;
@@ -1,86 +0,0 @@
1
- import type { CopyOption, DataSource, DeleteRowMode, SessionType } from "@vuu-ui/vuu-data-types";
2
- import type { RpcResult, VuuRowDataItemType } from "@vuu-ui/vuu-protocol-types";
3
- import { EventEmitter } from "@vuu-ui/vuu-utils";
4
- export type EditState = "clean" | "dirty" | "invalid" | "stale";
5
- export type EditActionType = "deleteRow" | "addRow" | "editCell";
6
- /** Column name to default value mapping applied to every addRow call when a column is absent from the row data. */
7
- export type RowDefaultDataItemValues = Record<string, VuuRowDataItemType>;
8
- export type EditSessionApi = "createSessionDataSource" | "beginEditSession";
9
- export type EditSessionConstructorProps = {
10
- dataSource: DataSource;
11
- /** @default "soft" */
12
- deleteMode?: DeleteRowMode;
13
- /** @default "createSessionDataSource" */
14
- editSessionApi?: EditSessionApi;
15
- /** Default column values merged into every addRow call for absent columns. Pass a stable reference. */
16
- rowDefaults?: RowDefaultDataItemValues;
17
- };
18
- export type NewRowState = {
19
- columns: readonly string[];
20
- requiredColumns?: readonly string[];
21
- draftRevision: number;
22
- errors: Readonly<Record<string, string>>;
23
- submitting: boolean;
24
- values: Readonly<Record<string, VuuRowDataItemType>>;
25
- };
26
- export type EditLifecycle = {
27
- status: "idle";
28
- } | {
29
- status: "starting";
30
- } | {
31
- status: "active";
32
- sessionDataSource: DataSource;
33
- } | {
34
- status: "ending";
35
- sessionDataSource: DataSource;
36
- } | {
37
- status: "error";
38
- operation: "begin" | "end";
39
- error: Error;
40
- sessionDataSource?: DataSource;
41
- };
42
- export declare class EditError extends Error {
43
- }
44
- export declare class SupersededEditError extends Error {
45
- }
46
- type EditSessionEvents = {
47
- cellEditChanged: (key: string, columnName: string) => void;
48
- editState: (editState: EditState) => void;
49
- lifecycle: (lifecycle: EditLifecycle) => void;
50
- newRow: (newRowState: NewRowState) => void;
51
- };
52
- export declare class EditSession extends EventEmitter<EditSessionEvents> {
53
- #private;
54
- static readonly newRowKey = "__vuu_new_row__";
55
- constructor({ dataSource, deleteMode, editSessionApi, rowDefaults, }: EditSessionConstructorProps);
56
- get editCount(): number;
57
- get invalidCount(): number;
58
- get deleteCount(): number;
59
- get addCount(): number;
60
- get editState(): EditState;
61
- get newRowState(): NewRowState;
62
- isNewRow(key: string): key is "__vuu_new_row__";
63
- isNewRowFinalColumn(columnName: string): boolean;
64
- isNewRowComplete(): boolean;
65
- configureNewRow(columns: readonly string[], requiredColumns?: readonly string[]): void;
66
- setNewRowValue(column: string, value: VuuRowDataItemType): void;
67
- addNewRow(): Promise<RpcResult>;
68
- deleteSelectedRows(): Promise<RpcResult>;
69
- addRow(rowData?: Record<string, VuuRowDataItemType>): Promise<RpcResult>;
70
- hasRowChanges(key: string): boolean;
71
- isCellEdited(key: string, columnName: string): boolean;
72
- undoRowChange(key: string, action: EditActionType): Promise<RpcResult | undefined>;
73
- begin(copyOption?: CopyOption, sessionType?: SessionType): Promise<DataSource>;
74
- /**
75
- * Safe to call repeatedly - schema of a remote session table typically arrives
76
- * after begin() resolves, so callers re-invoke this once 'subscribed' fires.
77
- */
78
- reconcileWithSessionSchema(): void;
79
- get dataSource(): import("@vuu-ui/vuu-data-types").DataSourceBase<import("@vuu-ui/vuu-data-types").DataSourceRowWithBigint> | undefined;
80
- get sessionDataSource(): DataSource | undefined;
81
- end(saveChanges?: boolean, force?: boolean): Promise<void>;
82
- get lifecycle(): EditLifecycle;
83
- get inEditMode(): boolean;
84
- commit(key: string, columnName: string, originalValue: VuuRowDataItemType, typedValue: string | number | boolean, isValid: boolean): Promise<RpcResult>;
85
- }
86
- export {};
@@ -1,12 +0,0 @@
1
- import type { TableCellRendererProps } from "@vuu-ui/vuu-table-types";
2
- export declare const UNDO_CELL_RENDERER = "vuu.undo-cell";
3
- export interface UndoCellRendererComponentProps {
4
- icon?: string;
5
- text?: string | false;
6
- }
7
- export declare const getUndoButtonContent: (componentProps?: UndoCellRendererComponentProps) => {
8
- icon: string | undefined;
9
- text: string | undefined;
10
- };
11
- export declare const getUndoTooltipContent: (action: unknown) => "Undo delete row" | "Undo insert row" | "Undo row edits" | undefined;
12
- export declare const UndoCellRenderer: ({ column, dataRow, }: TableCellRendererProps) => import("react").JSX.Element | null;
@@ -1,5 +0,0 @@
1
- import type { DataRow, RuntimeColumnDescriptor } from "@vuu-ui/vuu-table-types";
2
- export { isInlineEditingSession } from "@vuu-ui/vuu-utils";
3
- export declare const isEditRowReadOnly: (dataRow: DataRow) => boolean;
4
- export declare const withDataRowEditErrors: <T extends DataRow>(dataRow: T, errors: Record<string, string>) => T;
5
- export declare const getVuuEditMessage: (dataRow: DataRow, column: RuntimeColumnDescriptor, originalValue: string) => string | import("react").JSX.Element | undefined;
@@ -1,3 +0,0 @@
1
- import type { DataRow } from "@vuu-ui/vuu-table-types";
2
- export declare const EDIT_ACTION_ROW_CLASS_NAME_GENERATOR = "vuu-edit-actions";
3
- export declare const editActionRowClassNameGenerator: (dataRow: DataRow) => "vuuTableRow-deleted" | "vuuTableRow-inserted" | undefined;
@@ -1,10 +0,0 @@
1
- export { DataEditingProvider, useEditSession } from "./DataEditingProvider";
2
- export { useCellEdited } from "./useCellEdited";
3
- export { getVuuEditMessage, isInlineEditingSession, isEditRowReadOnly, withDataRowEditErrors, } from "./edit-utils";
4
- export { EditButtons, type EditButtonProps } from "./EditButtons";
5
- export { EditModeProvider, useEditMode, type EditModeContextProps, } from "./EditModeProvider";
6
- export { EditError, EditSession, SupersededEditError, type EditActionType, type EditLifecycle, type EditSessionConstructorProps, type NewRowState, type EditState, type RowDefaultDataItemValues, } from "./EditSession";
7
- export { StaleUpdateError } from "@vuu-ui/vuu-utils";
8
- export { EDIT_ACTION_ROW_CLASS_NAME_GENERATOR, editActionRowClassNameGenerator, } from "./editActionRowClassNameGenerator";
9
- export { getUndoButtonContent, getUndoTooltipContent, UNDO_CELL_RENDERER, UndoCellRenderer, type UndoCellRendererComponentProps, } from "./UndoCellRenderer";
10
- export { useEditableTable, type EditableTableHookProps, type EditMode, } from "./useEditableTable";
@@ -1,2 +0,0 @@
1
- import type { EditSession } from "./EditSession";
2
- export declare const useCellEdited: (editSession: EditSession | undefined, key: string, columnName: string) => boolean;
@@ -1,44 +0,0 @@
1
- import type { CopyOption, DataSource, DeleteRowMode } from "@vuu-ui/vuu-data-types";
2
- import type { VuuTable } from "@vuu-ui/vuu-protocol-types";
3
- import { EditSession, type EditActionType, type EditLifecycle, type EditSessionApi, type RowDefaultDataItemValues } from "./EditSession";
4
- export type EditMode = "edit" | "view";
5
- export interface EditableTableHookProps {
6
- /**
7
- * columns to be included in subscription. If not provided,
8
- * default will be '*'. Ignored if dataSource prop present.
9
- */
10
- columns?: string[];
11
- dataSource?: DataSource;
12
- addRowsCount?: number;
13
- deleteMode?: DeleteRowMode;
14
- editSessionApi?: EditSessionApi;
15
- copyOption?: CopyOption;
16
- isEditMode: boolean;
17
- onCancel: () => void;
18
- onSave: () => void;
19
- /** Default column values applied to every addRow call. Pass a stable reference — a new object triggers EditSession recreation. */
20
- rowDefaults?: RowDefaultDataItemValues;
21
- /**
22
- * If dataSource not provided, new DataSource
23
- * will be created using table and columns
24
- */
25
- table?: VuuTable;
26
- }
27
- export declare const useEditableTable: ({ columns, dataSource: dataSourceProp, deleteMode, editSessionApi, copyOption, isEditMode, onCancel, onSave, rowDefaults, table, }: EditableTableHookProps) => {
28
- canCancel: boolean;
29
- canSave: boolean;
30
- columnsDiverge: boolean;
31
- dataSource: DataSource;
32
- editSchema: import("@vuu-ui/vuu-data-types").TableSchema | undefined;
33
- editSession: EditSession;
34
- lifecycle: EditLifecycle;
35
- hasSelection: boolean;
36
- isEditSessionReady: boolean;
37
- onCancel: () => Promise<void>;
38
- onDelete: () => Promise<void>;
39
- onSave: (force?: boolean) => Promise<void>;
40
- onUndoRowChange: (key: string, action: EditActionType) => undefined;
41
- rowClassNameGenerators: string[] | undefined;
42
- sessionDataSource: import("@vuu-ui/vuu-data-types").DataSourceBase<import("@vuu-ui/vuu-data-types").DataSourceRowWithBigint> | undefined;
43
- sourceDataSource: DataSource;
44
- };