@vuu-ui/vuu-data-editing 3.0.0-beta.3 → 3.1.0-beta.1

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 (51) hide show
  1. package/package.json +15 -12
  2. package/src/DataEditingProvider.js +13 -0
  3. package/src/EditButtons.js +58 -0
  4. package/src/EditModeProvider.js +18 -0
  5. package/src/EditSession.js +482 -0
  6. package/src/UndoCellRenderer.js +38 -0
  7. package/src/edit-utils.js +47 -0
  8. package/src/editActionRowClassNameGenerator.js +15 -0
  9. package/src/index.js +10 -0
  10. package/src/useCellEdited.js +22 -0
  11. package/src/useEditableTable.js +141 -0
  12. package/cjs/DataEditingProvider.js +0 -28
  13. package/cjs/DataEditingProvider.js.map +0 -1
  14. package/cjs/EditButtons.js +0 -59
  15. package/cjs/EditButtons.js.map +0 -1
  16. package/cjs/EditModeProvider.js +0 -18
  17. package/cjs/EditModeProvider.js.map +0 -1
  18. package/cjs/EditSession.js +0 -564
  19. package/cjs/EditSession.js.map +0 -1
  20. package/cjs/UndoCellRenderer.js +0 -48
  21. package/cjs/UndoCellRenderer.js.map +0 -1
  22. package/cjs/edit-utils.js +0 -52
  23. package/cjs/edit-utils.js.map +0 -1
  24. package/cjs/editActionRowClassNameGenerator.js +0 -25
  25. package/cjs/editActionRowClassNameGenerator.js.map +0 -1
  26. package/cjs/index.js +0 -43
  27. package/cjs/index.js.map +0 -1
  28. package/cjs/useCellEdited.js +0 -29
  29. package/cjs/useCellEdited.js.map +0 -1
  30. package/cjs/useEditableTable.js +0 -145
  31. package/cjs/useEditableTable.js.map +0 -1
  32. package/esm/DataEditingProvider.js +0 -25
  33. package/esm/DataEditingProvider.js.map +0 -1
  34. package/esm/EditButtons.js +0 -57
  35. package/esm/EditButtons.js.map +0 -1
  36. package/esm/EditModeProvider.js +0 -15
  37. package/esm/EditModeProvider.js.map +0 -1
  38. package/esm/EditSession.js +0 -560
  39. package/esm/EditSession.js.map +0 -1
  40. package/esm/UndoCellRenderer.js +0 -43
  41. package/esm/UndoCellRenderer.js.map +0 -1
  42. package/esm/edit-utils.js +0 -45
  43. package/esm/edit-utils.js.map +0 -1
  44. package/esm/editActionRowClassNameGenerator.js +0 -22
  45. package/esm/editActionRowClassNameGenerator.js.map +0 -1
  46. package/esm/index.js +0 -11
  47. package/esm/index.js.map +0 -1
  48. package/esm/useCellEdited.js +0 -27
  49. package/esm/useCellEdited.js.map +0 -1
  50. package/esm/useEditableTable.js +0 -143
  51. package/esm/useEditableTable.js.map +0 -1
package/package.json CHANGED
@@ -1,18 +1,24 @@
1
1
  {
2
- "version": "3.0.0-beta.3",
2
+ "version": "3.1.0-beta.1",
3
3
  "description": "Shared data editing APIs and React components for VUU UI",
4
+ "main": "src/index.ts",
4
5
  "author": "heswell",
5
6
  "license": "Apache-2.0",
6
7
  "type": "module",
8
+ "scripts": {
9
+ "build": "node ../../scripts/run-build-rslib.ts",
10
+ "test": "vitest run",
11
+ "type-defs": "node ../../scripts/build-type-defs.mjs"
12
+ },
7
13
  "dependencies": {
8
14
  "@salt-ds/core": "1.54.1",
9
- "@vuu-ui/vuu-ui-controls": "3.0.0-beta.3",
10
- "@vuu-ui/vuu-utils": "3.0.0-beta.3"
15
+ "@vuu-ui/vuu-ui-controls": "3.1.0-beta.1",
16
+ "@vuu-ui/vuu-utils": "3.1.0-beta.1"
11
17
  },
12
18
  "devDependencies": {
13
- "@vuu-ui/vuu-data-types": "3.0.0-beta.3",
14
- "@vuu-ui/vuu-protocol-types": "3.0.0-beta.3",
15
- "@vuu-ui/vuu-table-types": "3.0.0-beta.3"
19
+ "@vuu-ui/vuu-data-types": "3.1.0-beta.1",
20
+ "@vuu-ui/vuu-protocol-types": "3.1.0-beta.1",
21
+ "@vuu-ui/vuu-table-types": "3.1.0-beta.1"
16
22
  },
17
23
  "peerDependencies": {
18
24
  "react": "^19.2.3"
@@ -20,19 +26,16 @@
20
26
  "sideEffects": false,
21
27
  "files": [
22
28
  "README.md",
23
- "esm",
24
- "cjs",
29
+ "src",
25
30
  "/types"
26
31
  ],
27
32
  "exports": {
28
33
  ".": {
29
- "require": "./cjs/index.js",
30
- "import": "./esm/index.js",
34
+ "import": "./src/index.js",
31
35
  "types": "./types/index.d.ts"
32
36
  }
33
37
  },
34
- "main": "cjs/index.js",
35
- "module": "esm/index.js",
38
+ "module": "index.js",
36
39
  "name": "@vuu-ui/vuu-data-editing",
37
40
  "types": "types/index.d.ts"
38
41
  }
@@ -0,0 +1,13 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext } from "react";
3
+ const DataEditingContext = /*#__PURE__*/ createContext(void 0);
4
+ const DataEditingProvider = ({ children, editSession })=>/*#__PURE__*/ jsx(DataEditingContext.Provider, {
5
+ value: editSession,
6
+ children: children
7
+ });
8
+ function useEditSession(throwIfUnavailable = false) {
9
+ 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");
12
+ }
13
+ export { DataEditingProvider, useEditSession };
@@ -0,0 +1,58 @@
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
+ import { Button } from "@salt-ds/core";
3
+ import { useCallback, useEffect, useState } from "react";
4
+ const EditButtons = ({ canCancel, canSave, confirmCancel, confirmSave, editSession, hasSelection = false, onCancel, onDelete, onSave, saveLabel = "Save" })=>{
5
+ const [editState, setEditState] = useState(()=>editSession?.editState ?? "clean");
6
+ const handleSave = useCallback(async ()=>{
7
+ if (confirmSave) {
8
+ const confirmed = await confirmSave();
9
+ if (!confirmed) return;
10
+ }
11
+ onSave("stale" === editState);
12
+ }, [
13
+ confirmSave,
14
+ editState,
15
+ onSave
16
+ ]);
17
+ const handleCancel = useCallback(async ()=>{
18
+ if (confirmCancel) {
19
+ const confirmed = await confirmCancel();
20
+ if (!confirmed) return;
21
+ }
22
+ onCancel?.();
23
+ }, [
24
+ confirmCancel,
25
+ onCancel
26
+ ]);
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
+ return /*#__PURE__*/ jsxs(Fragment, {
37
+ children: [
38
+ onDelete && /*#__PURE__*/ jsx(Button, {
39
+ disabled: !hasSelection,
40
+ onClick: onDelete,
41
+ sentiment: "negative",
42
+ children: "Delete"
43
+ }),
44
+ /*#__PURE__*/ jsx(Button, {
45
+ disabled: !canSave,
46
+ onClick: handleSave,
47
+ sentiment: "accented",
48
+ children: "stale" === editState ? `${saveLabel} (force)` : saveLabel
49
+ }),
50
+ onCancel && /*#__PURE__*/ jsx(Button, {
51
+ disabled: !canCancel,
52
+ onClick: handleCancel,
53
+ children: "Cancel"
54
+ })
55
+ ]
56
+ });
57
+ };
58
+ export { EditButtons };
@@ -0,0 +1,18 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useState } from "react";
3
+ const EditModeContext = /*#__PURE__*/ createContext({
4
+ isEditMode: false,
5
+ setEditMode: ()=>"EditModeProvider in place"
6
+ });
7
+ const EditModeProvider = ({ children })=>{
8
+ const [isEditMode, setEditMode] = useState(false);
9
+ return /*#__PURE__*/ jsx(EditModeContext.Provider, {
10
+ value: {
11
+ isEditMode,
12
+ setEditMode
13
+ },
14
+ children: children
15
+ });
16
+ };
17
+ const useEditMode = ()=>useContext(EditModeContext);
18
+ export { EditModeProvider, useEditMode };
@@ -0,0 +1,482 @@
1
+ import { EventEmitter, StaleUpdateError, isRpcError } from "@vuu-ui/vuu-utils";
2
+ const toEditSessionMode = (copyOption)=>{
3
+ switch(copyOption){
4
+ case "All":
5
+ return "all-rows";
6
+ case "Selected":
7
+ return "selected-rows";
8
+ case "Empty":
9
+ return "empty-session-table";
10
+ }
11
+ };
12
+ class EditError extends Error {
13
+ }
14
+ class SupersededEditError extends Error {
15
+ }
16
+ class EditSession extends EventEmitter {
17
+ static newRowKey = "__vuu_new_row__";
18
+ #rowEdits = new Map();
19
+ #deletedRows = new Set();
20
+ #deleteRevision = 0;
21
+ #rowDeleteRevisions = new Map();
22
+ #editCount = 0;
23
+ #deleteCount = 0;
24
+ #addCount = 0;
25
+ #invalidCount = 0;
26
+ #isStale = false;
27
+ #commitRevision = 0;
28
+ #cellCommitRevisions = new Map();
29
+ #deleteMode;
30
+ #editSessionApi;
31
+ #sourceTableDataSource;
32
+ #sessionDataSource;
33
+ #newRowState = {
34
+ columns: [],
35
+ draftRevision: 0,
36
+ errors: {},
37
+ submitting: false,
38
+ values: {}
39
+ };
40
+ #lifecycle = {
41
+ status: "idle"
42
+ };
43
+ #transitionQueue = Promise.resolve();
44
+ constructor(dataSource, deleteMode = "soft", editSessionApi = "createSessionDataSource"){
45
+ super();
46
+ this.#sourceTableDataSource = dataSource;
47
+ this.#deleteMode = deleteMode;
48
+ this.#editSessionApi = editSessionApi;
49
+ }
50
+ get editCount() {
51
+ return this.#editCount;
52
+ }
53
+ set editCount(val) {
54
+ this.#setEditCounts(val, this.#invalidCount);
55
+ }
56
+ get invalidCount() {
57
+ return this.#invalidCount;
58
+ }
59
+ set invalidCount(val) {
60
+ this.#setEditCounts(this.#editCount, val);
61
+ }
62
+ get deleteCount() {
63
+ return this.#deleteCount;
64
+ }
65
+ set deleteCount(val) {
66
+ if (val !== this.#deleteCount) {
67
+ const oldState = this.editState;
68
+ const oldCount = this.#deleteCount;
69
+ this.#deleteCount = val;
70
+ this.#emitEditStateChange(oldState, 0 === oldCount || 0 === val);
71
+ }
72
+ }
73
+ get addCount() {
74
+ return this.#addCount;
75
+ }
76
+ set addCount(val) {
77
+ if (val !== this.#addCount) {
78
+ const oldState = this.editState;
79
+ const oldCount = this.#addCount;
80
+ this.#addCount = val;
81
+ this.#emitEditStateChange(oldState, 0 === oldCount || 0 === val);
82
+ }
83
+ }
84
+ get editState() {
85
+ if (this.#invalidCount > 0) return "invalid";
86
+ if (this.#isStale) return "stale";
87
+ return 0 === this.#editCount && 0 === this.#deleteCount && 0 === this.#addCount ? "clean" : "dirty";
88
+ }
89
+ #emitEditStateChange(oldState, force = false) {
90
+ const newState = this.editState;
91
+ if (force || newState !== oldState) this.emit("editState", newState);
92
+ }
93
+ #setEditCounts(editCount, invalidCount) {
94
+ if (editCount !== this.#editCount || invalidCount !== this.#invalidCount) {
95
+ const oldState = this.editState;
96
+ this.#editCount = editCount;
97
+ this.#invalidCount = invalidCount;
98
+ this.#emitEditStateChange(oldState);
99
+ }
100
+ }
101
+ #setStale(isStale) {
102
+ if (isStale !== this.#isStale) {
103
+ const oldState = this.editState;
104
+ this.#isStale = isStale;
105
+ this.#emitEditStateChange(oldState);
106
+ }
107
+ }
108
+ #refreshEditCounts() {
109
+ let editCount = 0;
110
+ let invalidCount = 0;
111
+ for (const { cellEdits } of this.#rowEdits.values())for (const cellEdit of cellEdits.values())if (cellEdit.isValid) editCount++;
112
+ else invalidCount++;
113
+ this.#setEditCounts(editCount, invalidCount);
114
+ }
115
+ get newRowState() {
116
+ return this.#newRowState;
117
+ }
118
+ isNewRow(key) {
119
+ return key === EditSession.newRowKey;
120
+ }
121
+ isNewRowFinalColumn(columnName) {
122
+ return this.#newRowState.columns.at(-1) === columnName;
123
+ }
124
+ isNewRowComplete() {
125
+ return this.#newRowState.columns.every((column)=>{
126
+ const value = this.#newRowState.values[column];
127
+ return void 0 !== value && ("string" != typeof value || "" !== value.trim());
128
+ });
129
+ }
130
+ configureNewRow(columns) {
131
+ if (columns.length === this.#newRowState.columns.length && columns.every((column, index)=>column === this.#newRowState.columns[index])) return;
132
+ const columnSet = new Set(columns);
133
+ const errors = Object.fromEntries(Object.entries(this.#newRowState.errors).filter(([column])=>columnSet.has(column)));
134
+ this.#setNewRowState({
135
+ ...this.#newRowState,
136
+ columns: [
137
+ ...columns
138
+ ],
139
+ errors
140
+ });
141
+ }
142
+ setNewRowValue(column, value) {
143
+ const errors = {
144
+ ...this.#newRowState.errors
145
+ };
146
+ delete errors[column];
147
+ this.#setNewRowState({
148
+ ...this.#newRowState,
149
+ errors,
150
+ values: {
151
+ ...this.#newRowState.values,
152
+ [column]: value
153
+ }
154
+ });
155
+ }
156
+ async addNewRow() {
157
+ const missingErrors = Object.fromEntries(this.#newRowState.columns.filter((column)=>{
158
+ const value = this.#newRowState.values[column];
159
+ return void 0 === value || "string" == typeof value && "" === value.trim();
160
+ }).map((column)=>[
161
+ column,
162
+ "Value required"
163
+ ]));
164
+ const errors = {
165
+ ...this.#newRowState.errors,
166
+ ...missingErrors
167
+ };
168
+ if (Object.keys(errors).length > 0) {
169
+ this.#setNewRowState({
170
+ ...this.#newRowState,
171
+ errors
172
+ });
173
+ return {
174
+ data: void 0,
175
+ type: "SUCCESS_RESULT"
176
+ };
177
+ }
178
+ if (this.#newRowState.submitting) return {
179
+ data: void 0,
180
+ type: "SUCCESS_RESULT"
181
+ };
182
+ this.#setNewRowState({
183
+ ...this.#newRowState,
184
+ submitting: true
185
+ });
186
+ try {
187
+ const response = await this.addRow({
188
+ ...this.#newRowState.values
189
+ });
190
+ if (isRpcError(response)) {
191
+ const finalColumn = this.#newRowState.columns.at(-1);
192
+ this.#setNewRowState({
193
+ ...this.#newRowState,
194
+ errors: finalColumn ? {
195
+ [finalColumn]: response.errorMessage
196
+ } : this.#newRowState.errors,
197
+ submitting: false
198
+ });
199
+ return response;
200
+ }
201
+ this.#setNewRowState({
202
+ columns: this.#newRowState.columns,
203
+ draftRevision: this.#newRowState.draftRevision + 1,
204
+ errors: {},
205
+ submitting: false,
206
+ values: {}
207
+ });
208
+ return response;
209
+ } catch (error) {
210
+ const errorMessage = error instanceof Error ? error.message : "Unable to add row";
211
+ const finalColumn = this.#newRowState.columns.at(-1);
212
+ this.#setNewRowState({
213
+ ...this.#newRowState,
214
+ errors: finalColumn ? {
215
+ [finalColumn]: errorMessage
216
+ } : this.#newRowState.errors,
217
+ submitting: false
218
+ });
219
+ return {
220
+ errorMessage,
221
+ type: "ERROR_RESULT"
222
+ };
223
+ }
224
+ }
225
+ #setNewRowState(newRowState) {
226
+ this.#newRowState = newRowState;
227
+ this.emit("newRow", newRowState);
228
+ }
229
+ async deleteSelectedRows() {
230
+ const response = await this.dataSource?.deleteSelectedRows?.(this.#deleteMode);
231
+ if (isRpcError(response)) return;
232
+ const deletedKeys = response?.data?.deletedKeys;
233
+ if (deletedKeys && deletedKeys.length > 0) {
234
+ let newCount = 0;
235
+ for (const key of deletedKeys){
236
+ this.#rowDeleteRevisions.set(key, ++this.#deleteRevision);
237
+ if (!this.#deletedRows.has(key)) {
238
+ this.#deletedRows.add(key);
239
+ newCount++;
240
+ }
241
+ }
242
+ if (newCount > 0) this.deleteCount = this.#deleteCount + newCount;
243
+ }
244
+ }
245
+ async addRow(rowData = {}) {
246
+ const addRow = this.dataSource?.addRow;
247
+ if (void 0 === addRow) throw Error("[EditSession] datasource does not support adding rows");
248
+ const response = await addRow.call(this.dataSource, rowData);
249
+ if (void 0 === response) throw Error("[EditSession] datasource returned no response when adding row");
250
+ if (!isRpcError(response)) this.addCount = this.#addCount + 1;
251
+ return response;
252
+ }
253
+ restoreRows(keys) {
254
+ for (const key of keys)if (this.#deletedRows.has(key)) {
255
+ this.#deletedRows.delete(key);
256
+ this.#rowDeleteRevisions.delete(key);
257
+ this.deleteCount = this.#deleteCount - 1;
258
+ }
259
+ }
260
+ hasRowChanges(key) {
261
+ return this.#rowEdits.has(key) || this.#deletedRows.has(key);
262
+ }
263
+ isCellEdited(key, columnName) {
264
+ const cellEdit = this.#rowEdits.get(key)?.cellEdits.get(columnName);
265
+ return cellEdit?.isValid === true && cellEdit.originalValue !== cellEdit.editedValue;
266
+ }
267
+ async undoRowChange(key) {
268
+ if (!this.inEditMode) return;
269
+ const undoRevision = this.#commitRevision;
270
+ const deleteRevisionAtRequest = this.#rowDeleteRevisions.get(key);
271
+ const rowEditsAtRequest = this.#rowEdits.get(key);
272
+ const columnsAtRequest = new Set(rowEditsAtRequest?.cellEdits.keys());
273
+ const wasDeleted = this.#deletedRows.has(key);
274
+ const response = await this.dataSource?.undoRowChange?.(key);
275
+ if (isRpcError(response)) return;
276
+ const rowEdits = this.#rowEdits.get(key);
277
+ if (rowEdits) {
278
+ const changedColumns = [];
279
+ for (const columnName of columnsAtRequest){
280
+ const latestRevision = this.#cellCommitRevisions.get(key)?.get(columnName) ?? 0;
281
+ if (!(latestRevision > undoRevision)) {
282
+ if (this.isCellEdited(key, columnName)) changedColumns.push(columnName);
283
+ rowEdits.cellEdits.delete(columnName);
284
+ this.#setCellCommitRevision(key, columnName);
285
+ }
286
+ }
287
+ if (0 === rowEdits.cellEdits.size) this.#rowEdits.delete(key);
288
+ this.#refreshEditCounts();
289
+ for (const columnName of changedColumns)this.emit("cellEditChanged", key, columnName);
290
+ }
291
+ if (wasDeleted && this.#deletedRows.has(key) && this.#rowDeleteRevisions.get(key) === deleteRevisionAtRequest) {
292
+ this.#deletedRows.delete(key);
293
+ this.#rowDeleteRevisions.delete(key);
294
+ this.deleteCount = this.#deleteCount - 1;
295
+ }
296
+ const wasInsertedRow = response?.data?.wasInsertedRow === true;
297
+ if (wasInsertedRow) this.addCount = this.#addCount - 1;
298
+ }
299
+ #clearEdits() {
300
+ const oldState = this.editState;
301
+ const editedCells = [
302
+ ...this.#rowEdits
303
+ ].flatMap(([key, { cellEdits }])=>[
304
+ ...cellEdits.keys()
305
+ ].filter((columnName)=>this.isCellEdited(key, columnName)).map((columnName)=>[
306
+ key,
307
+ columnName
308
+ ]));
309
+ this.#rowEdits.clear();
310
+ this.#deletedRows.clear();
311
+ this.#rowDeleteRevisions.clear();
312
+ this.#cellCommitRevisions.clear();
313
+ this.#editCount = 0;
314
+ this.#deleteCount = 0;
315
+ this.#addCount = 0;
316
+ this.#invalidCount = 0;
317
+ this.#isStale = false;
318
+ this.#setNewRowState({
319
+ columns: this.#newRowState.columns,
320
+ draftRevision: this.#newRowState.draftRevision + 1,
321
+ errors: {},
322
+ submitting: false,
323
+ values: {}
324
+ });
325
+ for (const [key, columnName] of editedCells)this.emit("cellEditChanged", key, columnName);
326
+ this.#emitEditStateChange(oldState);
327
+ }
328
+ #setLifecycle(lifecycle) {
329
+ this.#lifecycle = lifecycle;
330
+ this.emit("lifecycle", lifecycle);
331
+ }
332
+ #enqueue(operation) {
333
+ const result = this.#transitionQueue.then(operation);
334
+ this.#transitionQueue = result.then(()=>void 0, ()=>void 0);
335
+ return result;
336
+ }
337
+ begin(copyOption = "All") {
338
+ return this.#enqueue(async ()=>{
339
+ if ("active" === this.#lifecycle.status || "error" === this.#lifecycle.status && "end" === this.#lifecycle.operation) {
340
+ const sessionDataSource = this.#sessionDataSource;
341
+ if (!sessionDataSource) throw new Error("[EditSession] active lifecycle has no datasource");
342
+ if ("error" === this.#lifecycle.status) this.#setLifecycle({
343
+ status: "active",
344
+ sessionDataSource
345
+ });
346
+ return sessionDataSource;
347
+ }
348
+ this.#setLifecycle({
349
+ status: "starting"
350
+ });
351
+ try {
352
+ const sourceDataSource = this.#sourceTableDataSource;
353
+ const sessionDataSource = "beginEditSession" === this.#editSessionApi ? await sourceDataSource?.beginEditSession?.(toEditSessionMode(copyOption)) : await sourceDataSource?.createSessionDataSource?.(copyOption);
354
+ if (!sessionDataSource) throw new Error(`[EditSession] datasource does not support ${this.#editSessionApi}`);
355
+ this.#sessionDataSource = sessionDataSource;
356
+ this.#setStale(false);
357
+ this.#setLifecycle({
358
+ status: "active",
359
+ sessionDataSource
360
+ });
361
+ return sessionDataSource;
362
+ } catch (cause) {
363
+ const error = cause instanceof Error ? cause : new Error(String(cause));
364
+ this.#setLifecycle({
365
+ status: "error",
366
+ operation: "begin",
367
+ error
368
+ });
369
+ throw error;
370
+ }
371
+ });
372
+ }
373
+ get dataSource() {
374
+ return this.#sessionDataSource ?? this.#sourceTableDataSource;
375
+ }
376
+ get sessionDataSource() {
377
+ return this.#sessionDataSource;
378
+ }
379
+ end(saveChanges = false, force = false) {
380
+ return this.#enqueue(async ()=>{
381
+ if ("idle" === this.#lifecycle.status || "error" === this.#lifecycle.status && "begin" === this.#lifecycle.operation) {
382
+ if ("idle" !== this.#lifecycle.status) this.#setLifecycle({
383
+ status: "idle"
384
+ });
385
+ return;
386
+ }
387
+ const sessionDataSource = this.#sessionDataSource;
388
+ if (!sessionDataSource) throw new Error("[EditSession] ending lifecycle has no datasource");
389
+ this.#setLifecycle({
390
+ status: "ending",
391
+ sessionDataSource
392
+ });
393
+ try {
394
+ await this.dataSource?.endEditSession?.(saveChanges, force);
395
+ this.#clearEdits();
396
+ this.#sessionDataSource = void 0;
397
+ this.#setLifecycle({
398
+ status: "idle"
399
+ });
400
+ } catch (cause) {
401
+ const error = cause instanceof Error ? cause : new Error(String(cause));
402
+ if (error instanceof StaleUpdateError) this.#setStale(true);
403
+ this.#setLifecycle({
404
+ status: "error",
405
+ operation: "end",
406
+ error,
407
+ sessionDataSource
408
+ });
409
+ throw error;
410
+ }
411
+ });
412
+ }
413
+ get lifecycle() {
414
+ return this.#lifecycle;
415
+ }
416
+ get inEditMode() {
417
+ return "active" === this.#lifecycle.status || "ending" === this.#lifecycle.status || "error" === this.#lifecycle.status && "end" === this.#lifecycle.operation;
418
+ }
419
+ #getOrCreateRowEdits(key) {
420
+ const rowEditDetails = this.#rowEdits.get(key);
421
+ if (rowEditDetails) return rowEditDetails;
422
+ {
423
+ const rowEditDetails = {
424
+ cellEdits: new Map()
425
+ };
426
+ this.#rowEdits.set(key, rowEditDetails);
427
+ return rowEditDetails;
428
+ }
429
+ }
430
+ #storeCellEdit(key, cellEdits, column, originalValue, editedValue, isValid) {
431
+ const wasEdited = this.isCellEdited(key, column);
432
+ const existingCellEdit = cellEdits.get(column);
433
+ const cellEdit = {
434
+ originalValue: existingCellEdit?.originalValue ?? originalValue,
435
+ editedValue,
436
+ isValid
437
+ };
438
+ if (isValid && cellEdit.originalValue === editedValue) cellEdits.delete(column);
439
+ else cellEdits.set(column, cellEdit);
440
+ this.#refreshEditCounts();
441
+ if (wasEdited !== this.isCellEdited(key, column)) this.emit("cellEditChanged", key, column);
442
+ return cellEdit;
443
+ }
444
+ #setCellCommitRevision(key, columnName) {
445
+ const revision = ++this.#commitRevision;
446
+ const rowRevisions = this.#cellCommitRevisions.get(key) ?? new Map();
447
+ rowRevisions.set(columnName, revision);
448
+ this.#cellCommitRevisions.set(key, rowRevisions);
449
+ return revision;
450
+ }
451
+ #isLatestCellCommit(key, columnName, revision) {
452
+ return this.#cellCommitRevisions.get(key)?.get(columnName) === revision;
453
+ }
454
+ async commit(key, columnName, originalValue, typedValue, isValid) {
455
+ if ("active" !== this.#lifecycle.status && !("error" === this.#lifecycle.status && "end" === this.#lifecycle.operation)) throw new Error("No edit session in progress");
456
+ const revision = this.#setCellCommitRevision(key, columnName);
457
+ const rowEditDetails = this.#getOrCreateRowEdits(key);
458
+ const { cellEdits } = rowEditDetails;
459
+ if (isValid) {
460
+ const cellEdit = this.#storeCellEdit(key, cellEdits, columnName, originalValue, typedValue, isValid);
461
+ if (this.dataSource?.editCell) {
462
+ const response = await this.dataSource.editCell(key, columnName, typedValue);
463
+ if (!this.#isLatestCellCommit(key, columnName, revision)) throw new SupersededEditError(`Edit response superseded for ${key}:${columnName}`);
464
+ const currentCellEdits = this.#getOrCreateRowEdits(key).cellEdits;
465
+ if (isRpcError(response)) this.#storeCellEdit(key, currentCellEdits, columnName, cellEdit.originalValue, typedValue, false);
466
+ else if (0 === currentCellEdits.size) this.#rowEdits.delete(key);
467
+ return response;
468
+ }
469
+ if (0 === cellEdits.size) this.#rowEdits.delete(key);
470
+ return {
471
+ data: void 0,
472
+ type: "SUCCESS_RESULT"
473
+ };
474
+ }
475
+ this.#storeCellEdit(key, cellEdits, columnName, originalValue, typedValue, isValid);
476
+ return {
477
+ data: void 0,
478
+ type: "SUCCESS_RESULT"
479
+ };
480
+ }
481
+ }
482
+ export { EditError, EditSession, SupersededEditError };
@@ -0,0 +1,38 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Button, Tooltip } from "@salt-ds/core";
3
+ import { Icon } from "@vuu-ui/vuu-ui-controls";
4
+ import { registerComponent } from "@vuu-ui/vuu-utils";
5
+ import { useEditSession } from "./DataEditingProvider.js";
6
+ const UNDO_CELL_RENDERER = "vuu.undo-cell";
7
+ const getUndoButtonContent = (componentProps)=>({
8
+ icon: componentProps?.icon,
9
+ text: componentProps?.text === void 0 ? "UNDO" : componentProps.text || void 0
10
+ });
11
+ const getUndoTooltipContent = (action)=>"deleteRow" === action ? "Undo delete row" : "addRow" === action ? "Undo insert row" : "editCell" === action ? "Undo row edits" : void 0;
12
+ const UndoCellRenderer = ({ column, dataRow })=>{
13
+ const editSession = useEditSession();
14
+ const renderer = column.type?.renderer;
15
+ const { icon, text } = getUndoButtonContent(renderer?.componentProps);
16
+ const tooltipContent = getUndoTooltipContent(dataRow.vuu_action);
17
+ if (void 0 === tooltipContent) return null;
18
+ return /*#__PURE__*/ jsx(Tooltip, {
19
+ content: tooltipContent,
20
+ children: /*#__PURE__*/ jsxs(Button, {
21
+ appearance: "transparent",
22
+ "aria-label": tooltipContent,
23
+ onClick: ()=>editSession?.undoRowChange(dataRow.key),
24
+ style: {
25
+ height: "100%",
26
+ width: "100%"
27
+ },
28
+ children: [
29
+ icon ? /*#__PURE__*/ jsx(Icon, {
30
+ name: icon
31
+ }) : null,
32
+ text
33
+ ]
34
+ })
35
+ });
36
+ };
37
+ registerComponent(UNDO_CELL_RENDERER, UndoCellRenderer, "cell-renderer");
38
+ export { UNDO_CELL_RENDERER, UndoCellRenderer, getUndoButtonContent, getUndoTooltipContent };