@vuu-ui/vuu-data-editing 4.0.0-alpha.3 → 4.0.0-alpha.30

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": "4.0.0-alpha.3",
2
+ "version": "4.0.0-alpha.30",
3
3
  "description": "Shared data editing APIs and React components for VUU UI",
4
4
  "main": "./src/index.js",
5
5
  "author": "heswell",
@@ -11,14 +11,14 @@
11
11
  "type-defs": "node ../../scripts/build-type-defs.mjs"
12
12
  },
13
13
  "dependencies": {
14
- "@salt-ds/core": "1.67.0",
15
- "@vuu-ui/vuu-ui-controls": "4.0.0-alpha.3",
16
- "@vuu-ui/vuu-utils": "4.0.0-alpha.3"
14
+ "@salt-ds/core": "1.72.0",
15
+ "@vuu-ui/vuu-ui-controls": "4.0.0-alpha.30",
16
+ "@vuu-ui/vuu-utils": "4.0.0-alpha.30"
17
17
  },
18
18
  "devDependencies": {
19
- "@vuu-ui/vuu-data-types": "4.0.0-alpha.3",
20
- "@vuu-ui/vuu-protocol-types": "4.0.0-alpha.3",
21
- "@vuu-ui/vuu-table-types": "4.0.0-alpha.3"
19
+ "@vuu-ui/vuu-data-types": "4.0.0-alpha.30",
20
+ "@vuu-ui/vuu-protocol-types": "4.0.0-alpha.30",
21
+ "@vuu-ui/vuu-table-types": "4.0.0-alpha.30"
22
22
  },
23
23
  "peerDependencies": {
24
24
  "react": "^19.2.3"
@@ -31,8 +31,9 @@
31
31
  ],
32
32
  "exports": {
33
33
  ".": {
34
+ "types": "./types/index.d.ts",
34
35
  "import": "./src/index.js",
35
- "types": "./types/index.d.ts"
36
+ "default": "./src/index.js"
36
37
  }
37
38
  },
38
39
  "module": "./src/index.js",
@@ -1,5 +1,6 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import { createContext, useContext } from "react";
3
+ import { EditSession } from "./EditSession.js";
3
4
  const DataEditingContext = /*#__PURE__*/ createContext(void 0);
4
5
  const DataEditingProvider = ({ children, editSession })=>/*#__PURE__*/ jsx(DataEditingContext.Provider, {
5
6
  value: editSession,
@@ -7,7 +8,8 @@ const DataEditingProvider = ({ children, editSession })=>/*#__PURE__*/ jsx(DataE
7
8
  });
8
9
  function useEditSession(throwIfUnavailable = false) {
9
10
  const editSession = useContext(DataEditingContext);
10
- if (void 0 !== editSession) return editSession;
11
- if (throwIfUnavailable) throw Error("[useEditSession] no DataEditingContext in scope. You need to enclose editable component(s) with DataEditingProvider");
11
+ if (editSession instanceof EditSession) return editSession;
12
+ if (throwIfUnavailable) throw Error(void 0 === editSession ? "[useEditSession] no DataEditingContext in scope. You need to enclose editable component(s) with DataEditingProvider" : "[useEditSession] DataEditingProvider does not provide an EditSession");
12
13
  }
13
- export { DataEditingProvider, useEditSession };
14
+ const useTableEditSession = ()=>useContext(DataEditingContext);
15
+ export { DataEditingProvider, useEditSession, useTableEditSession };
@@ -0,0 +1,35 @@
1
+ import { EventEmitter } from "@vuu-ui/vuu-utils";
2
+ class DirectEditSession extends EventEmitter {
3
+ #dataSource;
4
+ constructor({ dataSource }){
5
+ super();
6
+ this.#dataSource = dataSource;
7
+ }
8
+ get dataSource() {
9
+ return this.#dataSource;
10
+ }
11
+ get inEditMode() {
12
+ return false;
13
+ }
14
+ isCellEdited(_key, _columnName) {
15
+ return false;
16
+ }
17
+ async cancel(_key, _columnName, _restoredValue) {
18
+ return {
19
+ data: void 0,
20
+ type: "SUCCESS_RESULT"
21
+ };
22
+ }
23
+ async commit(key, columnName, _originalValue, typedValue, isValid) {
24
+ if (!isValid) return {
25
+ errorMessage: "Invalid value",
26
+ type: "ERROR_RESULT"
27
+ };
28
+ const editCell = this.#dataSource.editCell;
29
+ if (void 0 === editCell) throw Error("[DirectEditSession] datasource does not support editCell");
30
+ const response = await editCell.call(this.#dataSource, key, columnName, typedValue);
31
+ if (void 0 === response) throw Error("[DirectEditSession] datasource returned no response from editCell");
32
+ return response;
33
+ }
34
+ }
35
+ export { DirectEditSession };
@@ -1,8 +1,10 @@
1
1
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
2
  import { Button } from "@salt-ds/core";
3
- import { useCallback, useEffect, useState } from "react";
3
+ import { useCallback } from "react";
4
+ import { useEditState } from "./useEditState.js";
4
5
  const EditButtons = ({ canCancel, canSave: _canSave, confirmCancel, confirmSave, editSession, hasSelection = false, onCancel, onDelete, onSave, saveLabel = "Save" })=>{
5
- const [editState, setEditState] = useState(()=>editSession?.editState ?? "clean");
6
+ const editState = useEditState(editSession);
7
+ const isDirty = editSession?.isDirty ?? false;
6
8
  const handleSave = useCallback(async ()=>{
7
9
  if (confirmSave) {
8
10
  const confirmed = await confirmSave();
@@ -15,24 +17,16 @@ const EditButtons = ({ canCancel, canSave: _canSave, confirmCancel, confirmSave,
15
17
  onSave
16
18
  ]);
17
19
  const handleCancel = useCallback(async ()=>{
18
- if (confirmCancel) {
20
+ if (confirmCancel && isDirty) {
19
21
  const confirmed = await confirmCancel();
20
22
  if (!confirmed) return;
21
23
  }
22
24
  onCancel?.();
23
25
  }, [
24
26
  confirmCancel,
27
+ isDirty,
25
28
  onCancel
26
29
  ]);
27
- useEffect(()=>{
28
- if (editSession) {
29
- setEditState(editSession.editState);
30
- editSession.on("editState", setEditState);
31
- return ()=>editSession.removeListener("editState", setEditState);
32
- }
33
- }, [
34
- editSession
35
- ]);
36
30
  return /*#__PURE__*/ jsxs(Fragment, {
37
31
  children: [
38
32
  onDelete && /*#__PURE__*/ jsx(Button, {
@@ -1,11 +1,16 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
- import { createContext, useContext, useState } from "react";
2
+ import { createContext, useContext, useEffect, useState } from "react";
3
3
  const EditModeContext = /*#__PURE__*/ createContext({
4
4
  isEditMode: false,
5
5
  setEditMode: ()=>"EditModeProvider in place"
6
6
  });
7
- const EditModeProvider = ({ children })=>{
8
- const [isEditMode, setEditMode] = useState(false);
7
+ const EditModeProvider = ({ children, isEditMode: isEditModeProp = false })=>{
8
+ const [isEditMode, setEditMode] = useState(isEditModeProp);
9
+ useEffect(()=>{
10
+ setEditMode(isEditModeProp);
11
+ }, [
12
+ isEditModeProp
13
+ ]);
9
14
  return /*#__PURE__*/ jsx(EditModeContext.Provider, {
10
15
  value: {
11
16
  isEditMode,
@@ -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;
@@ -66,6 +67,9 @@ class EditSession extends EventEmitter {
66
67
  if (this.#isStale) return "stale";
67
68
  return 0 === this.#editCount && 0 === this.#deleteCount && 0 === this.#addCount ? "clean" : "dirty";
68
69
  }
70
+ get isDirty() {
71
+ return "clean" !== this.editState;
72
+ }
69
73
  get canCancel() {
70
74
  return "active" === this.lifecycle.status || "error" === this.lifecycle.status && "end" === this.lifecycle.operation;
71
75
  }
@@ -85,19 +89,21 @@ class EditSession extends EventEmitter {
85
89
  }
86
90
  }
87
91
  #setDeleteCount(val) {
88
- if (val !== this.#deleteCount) {
92
+ const newCount = Math.max(0, val);
93
+ if (newCount !== this.#deleteCount) {
89
94
  const oldState = this.editState;
90
95
  const oldCount = this.#deleteCount;
91
- this.#deleteCount = val;
92
- this.#emitEditStateChange(oldState, 0 === oldCount || 0 === val);
96
+ this.#deleteCount = newCount;
97
+ this.#emitEditStateChange(oldState, 0 === oldCount || 0 === newCount);
93
98
  }
94
99
  }
95
100
  #setAddCount(val) {
96
- if (val !== this.#addCount) {
101
+ const newCount = Math.max(0, val);
102
+ if (newCount !== this.#addCount) {
97
103
  const oldState = this.editState;
98
104
  const oldCount = this.#addCount;
99
- this.#addCount = val;
100
- this.#emitEditStateChange(oldState, 0 === oldCount || 0 === val);
105
+ this.#addCount = newCount;
106
+ this.#emitEditStateChange(oldState, 0 === oldCount || 0 === newCount);
101
107
  }
102
108
  }
103
109
  #setStale(isStale) {
@@ -231,10 +237,17 @@ class EditSession extends EventEmitter {
231
237
  const deleteSelectedRows = this.dataSource?.deleteSelectedRows;
232
238
  if (void 0 === deleteSelectedRows) throw Error("[EditSession] datasource does not support deleting rows");
233
239
  const selectedRowsCount = this.dataSource?.selectedRowsCount ?? 0;
240
+ if (0 === selectedRowsCount) return {
241
+ data: void 0,
242
+ type: "SUCCESS_RESULT"
243
+ };
234
244
  const response = await deleteSelectedRows.call(this.dataSource, this.#deleteMode);
235
245
  if (void 0 === response) throw Error("[EditSession] datasource returned no response when deleting rows");
236
246
  if (isRpcError(response)) return response;
237
247
  if (selectedRowsCount > 0) this.#setDeleteCount(this.#deleteCount + selectedRowsCount);
248
+ await this.dataSource?.select?.({
249
+ type: "DESELECT_ALL"
250
+ });
238
251
  return response;
239
252
  }
240
253
  async addRow(rowData = {}) {
@@ -245,7 +258,11 @@ class EditSession extends EventEmitter {
245
258
  ...rowData
246
259
  });
247
260
  if (void 0 === response) throw Error("[EditSession] datasource returned no response when adding row");
248
- 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
+ }
249
266
  return response;
250
267
  }
251
268
  hasRowChanges(key) {
@@ -284,9 +301,12 @@ class EditSession extends EventEmitter {
284
301
  this.#invalidCount -= removedInvalidCount;
285
302
  for (const columnName of changedColumns)this.emit("cellEditChanged", key, columnName);
286
303
  }
287
- if ("deleteRow" === action) this.#deleteCount--;
288
- const wasInsertedRow = "addRow" === action || response?.data?.wasInsertedRow === true;
289
- 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
+ }
290
310
  this.#emitEditStateChange(oldState);
291
311
  return response;
292
312
  }
@@ -301,6 +321,7 @@ class EditSession extends EventEmitter {
301
321
  columnName
302
322
  ]));
303
323
  this.#rowEdits.clear();
324
+ this.#addedRowKeys.clear();
304
325
  this.#cellCommitRevisions.clear();
305
326
  this.#editCount = 0;
306
327
  this.#deleteCount = 0;
@@ -399,8 +420,10 @@ class EditSession extends EventEmitter {
399
420
  sessionDataSource
400
421
  });
401
422
  try {
402
- const response = await this.dataSource?.endEditSession?.(saveChanges, force);
403
- if (isRpcError(response)) throw new Error(response.errorMessage);
423
+ if ("unsubscribed" !== sessionDataSource.status || saveChanges) {
424
+ const response = await this.dataSource?.endEditSession?.(saveChanges, force);
425
+ if (isRpcError(response)) throw new Error(response.errorMessage);
426
+ }
404
427
  this.#clearEdits();
405
428
  this.#sessionDataSource = void 0;
406
429
  this.#setLifecycle({
@@ -475,6 +498,15 @@ class EditSession extends EventEmitter {
475
498
  #isLatestCellCommit(key, columnName, revision) {
476
499
  return this.#cellCommitRevisions.get(key)?.get(columnName) === revision;
477
500
  }
501
+ async cancel(key, columnName, restoredValue) {
502
+ const cellEdits = this.#rowEdits.get(key)?.cellEdits;
503
+ const cellEdit = cellEdits?.get(columnName);
504
+ if (this.inEditMode && cellEdits && cellEdit && !cellEdit.isValid) this.#storeCellEdit(key, cellEdits, columnName, cellEdit.originalValue, restoredValue, true);
505
+ return {
506
+ data: void 0,
507
+ type: "SUCCESS_RESULT"
508
+ };
509
+ }
478
510
  async commit(key, columnName, originalValue, typedValue, isValid, { dataSourceValue, syncToDataSource = true } = {}) {
479
511
  if ("active" !== this.#lifecycle.status && !("error" === this.#lifecycle.status && "end" === this.#lifecycle.operation)) throw new Error("No edit session in progress");
480
512
  const revision = this.#setCellCommitRevision(key, columnName);
File without changes
@@ -0,0 +1,13 @@
1
+ const css = `
2
+ .vuuEditField {
3
+ & .saltDropdown {
4
+ --vuu-icon-height: 15px;
5
+ --vuu-icon-left: calc(100% - var(--salt-spacing-300));
6
+ --vuu-icon-size: 8px;
7
+ --vuu-icon-top: -3px;
8
+ }
9
+ }
10
+
11
+
12
+ `;
13
+ export default css;