@vuu-ui/vuu-data-editing 4.0.0-alpha.1 → 4.0.0-alpha.11
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/package.json +7 -6
- package/src/EditButtons.js +8 -14
- package/src/EditModeProvider.js +8 -3
- package/src/EditSession.js +87 -82
- package/src/UndoCellRenderer.js +1 -1
- package/src/edit-field/EditField.css.js +13 -0
- package/src/edit-field/EditField.js +102 -0
- package/src/edit-field/useEditField.js +223 -0
- package/src/index.js +8 -3
- package/src/lookup-values/useLookupValues.js +78 -0
- package/src/useEditState.js +15 -0
- package/src/useEditable.js +52 -0
- package/src/useEditableTable.js +1 -1
- package/types/EditButtons.d.ts +1 -1
- package/types/EditModeProvider.d.ts +3 -2
- package/types/EditSession.d.ts +15 -11
- package/types/edit-field/EditField.d.ts +21 -0
- package/types/edit-field/useEditField.d.ts +26 -0
- package/types/index.d.ts +13 -8
- package/types/lookup-values/useLookupValues.d.ts +19 -0
- package/types/useEditState.d.ts +2 -0
- package/types/useEditable.d.ts +13 -0
- package/types/useEditableTable.d.ts +2 -2
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "4.0.0-alpha.
|
|
2
|
+
"version": "4.0.0-alpha.11",
|
|
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.67.0",
|
|
15
|
-
"@vuu-ui/vuu-ui-controls": "4.0.0-alpha.
|
|
16
|
-
"@vuu-ui/vuu-utils": "4.0.0-alpha.
|
|
15
|
+
"@vuu-ui/vuu-ui-controls": "4.0.0-alpha.11",
|
|
16
|
+
"@vuu-ui/vuu-utils": "4.0.0-alpha.11"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
|
-
"@vuu-ui/vuu-data-types": "4.0.0-alpha.
|
|
20
|
-
"@vuu-ui/vuu-protocol-types": "4.0.0-alpha.
|
|
21
|
-
"@vuu-ui/vuu-table-types": "4.0.0-alpha.
|
|
19
|
+
"@vuu-ui/vuu-data-types": "4.0.0-alpha.11",
|
|
20
|
+
"@vuu-ui/vuu-protocol-types": "4.0.0-alpha.11",
|
|
21
|
+
"@vuu-ui/vuu-table-types": "4.0.0-alpha.11"
|
|
22
22
|
},
|
|
23
23
|
"peerDependencies": {
|
|
24
24
|
"react": "^19.2.3"
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"exports": {
|
|
33
33
|
".": {
|
|
34
34
|
"import": "./src/index.js",
|
|
35
|
+
"default": "./src/index.js",
|
|
35
36
|
"types": "./types/index.d.ts"
|
|
36
37
|
}
|
|
37
38
|
},
|
package/src/EditButtons.js
CHANGED
|
@@ -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
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import { useCallback } from "react";
|
|
4
|
+
import { useEditState } from "./useEditState.js";
|
|
5
|
+
const EditButtons = ({ canCancel, canSave: _canSave, confirmCancel, confirmSave, editSession, hasSelection = false, onCancel, onDelete, onSave, saveLabel = "Save" })=>{
|
|
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, confirmCancel, confirmSave, editSessi
|
|
|
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, {
|
|
@@ -42,7 +36,7 @@ const EditButtons = ({ canCancel, canSave, confirmCancel, confirmSave, editSessi
|
|
|
42
36
|
children: "Delete"
|
|
43
37
|
}),
|
|
44
38
|
/*#__PURE__*/ jsx(Button, {
|
|
45
|
-
disabled: !canSave,
|
|
39
|
+
disabled: !editSession?.canSave,
|
|
46
40
|
onClick: handleSave,
|
|
47
41
|
sentiment: "accented",
|
|
48
42
|
children: "stale" === editState ? `${saveLabel} (force)` : saveLabel
|
package/src/EditModeProvider.js
CHANGED
|
@@ -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(
|
|
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,
|
package/src/EditSession.js
CHANGED
|
@@ -16,9 +16,6 @@ class SupersededEditError extends Error {
|
|
|
16
16
|
class EditSession extends EventEmitter {
|
|
17
17
|
static newRowKey = "__vuu_new_row__";
|
|
18
18
|
#rowEdits = new Map();
|
|
19
|
-
#deletedRows = new Set();
|
|
20
|
-
#deleteRevision = 0;
|
|
21
|
-
#rowDeleteRevisions = new Map();
|
|
22
19
|
#editCount = 0;
|
|
23
20
|
#deleteCount = 0;
|
|
24
21
|
#addCount = 0;
|
|
@@ -48,47 +45,36 @@ class EditSession extends EventEmitter {
|
|
|
48
45
|
this.#sourceTableDataSource = dataSource;
|
|
49
46
|
this.#deleteMode = deleteMode;
|
|
50
47
|
this.#editSessionApi = editSessionApi;
|
|
51
|
-
this.#rowDefaults =
|
|
48
|
+
this.#rowDefaults = {
|
|
49
|
+
...rowDefaults
|
|
50
|
+
};
|
|
52
51
|
}
|
|
53
52
|
get editCount() {
|
|
54
53
|
return this.#editCount;
|
|
55
54
|
}
|
|
56
|
-
set editCount(val) {
|
|
57
|
-
this.#setEditCounts(val, this.#invalidCount);
|
|
58
|
-
}
|
|
59
55
|
get invalidCount() {
|
|
60
56
|
return this.#invalidCount;
|
|
61
57
|
}
|
|
62
|
-
set invalidCount(val) {
|
|
63
|
-
this.#setEditCounts(this.#editCount, val);
|
|
64
|
-
}
|
|
65
58
|
get deleteCount() {
|
|
66
59
|
return this.#deleteCount;
|
|
67
60
|
}
|
|
68
|
-
set deleteCount(val) {
|
|
69
|
-
if (val !== this.#deleteCount) {
|
|
70
|
-
const oldState = this.editState;
|
|
71
|
-
const oldCount = this.#deleteCount;
|
|
72
|
-
this.#deleteCount = val;
|
|
73
|
-
this.#emitEditStateChange(oldState, 0 === oldCount || 0 === val);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
61
|
get addCount() {
|
|
77
62
|
return this.#addCount;
|
|
78
63
|
}
|
|
79
|
-
set addCount(val) {
|
|
80
|
-
if (val !== this.#addCount) {
|
|
81
|
-
const oldState = this.editState;
|
|
82
|
-
const oldCount = this.#addCount;
|
|
83
|
-
this.#addCount = val;
|
|
84
|
-
this.#emitEditStateChange(oldState, 0 === oldCount || 0 === val);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
64
|
get editState() {
|
|
88
65
|
if (this.#invalidCount > 0) return "invalid";
|
|
89
66
|
if (this.#isStale) return "stale";
|
|
90
67
|
return 0 === this.#editCount && 0 === this.#deleteCount && 0 === this.#addCount ? "clean" : "dirty";
|
|
91
68
|
}
|
|
69
|
+
get isDirty() {
|
|
70
|
+
return "clean" !== this.editState;
|
|
71
|
+
}
|
|
72
|
+
get canCancel() {
|
|
73
|
+
return "active" === this.lifecycle.status || "error" === this.lifecycle.status && "end" === this.lifecycle.operation;
|
|
74
|
+
}
|
|
75
|
+
get canSave() {
|
|
76
|
+
return this.canCancel && ("dirty" === this.editState || "stale" === this.editState) && 0 === this.invalidCount;
|
|
77
|
+
}
|
|
92
78
|
#emitEditStateChange(oldState, force = false) {
|
|
93
79
|
const newState = this.editState;
|
|
94
80
|
if (force || newState !== oldState) this.emit("editState", newState);
|
|
@@ -101,6 +87,22 @@ class EditSession extends EventEmitter {
|
|
|
101
87
|
this.#emitEditStateChange(oldState);
|
|
102
88
|
}
|
|
103
89
|
}
|
|
90
|
+
#setDeleteCount(val) {
|
|
91
|
+
if (val !== this.#deleteCount) {
|
|
92
|
+
const oldState = this.editState;
|
|
93
|
+
const oldCount = this.#deleteCount;
|
|
94
|
+
this.#deleteCount = val;
|
|
95
|
+
this.#emitEditStateChange(oldState, 0 === oldCount || 0 === val);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
#setAddCount(val) {
|
|
99
|
+
if (val !== this.#addCount) {
|
|
100
|
+
const oldState = this.editState;
|
|
101
|
+
const oldCount = this.#addCount;
|
|
102
|
+
this.#addCount = val;
|
|
103
|
+
this.#emitEditStateChange(oldState, 0 === oldCount || 0 === val);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
104
106
|
#setStale(isStale) {
|
|
105
107
|
if (isStale !== this.#isStale) {
|
|
106
108
|
const oldState = this.editState;
|
|
@@ -108,13 +110,6 @@ class EditSession extends EventEmitter {
|
|
|
108
110
|
this.#emitEditStateChange(oldState);
|
|
109
111
|
}
|
|
110
112
|
}
|
|
111
|
-
#refreshEditCounts() {
|
|
112
|
-
let editCount = 0;
|
|
113
|
-
let invalidCount = 0;
|
|
114
|
-
for (const { cellEdits } of this.#rowEdits.values())for (const cellEdit of cellEdits.values())if (cellEdit.isValid) editCount++;
|
|
115
|
-
else invalidCount++;
|
|
116
|
-
this.#setEditCounts(editCount, invalidCount);
|
|
117
|
-
}
|
|
118
113
|
get newRowState() {
|
|
119
114
|
return this.#newRowState;
|
|
120
115
|
}
|
|
@@ -236,20 +231,14 @@ class EditSession extends EventEmitter {
|
|
|
236
231
|
this.emit("newRow", newRowState);
|
|
237
232
|
}
|
|
238
233
|
async deleteSelectedRows() {
|
|
239
|
-
const
|
|
240
|
-
if (
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
this.#deletedRows.add(key);
|
|
248
|
-
newCount++;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
if (newCount > 0) this.deleteCount = this.#deleteCount + newCount;
|
|
252
|
-
}
|
|
234
|
+
const deleteSelectedRows = this.dataSource?.deleteSelectedRows;
|
|
235
|
+
if (void 0 === deleteSelectedRows) throw Error("[EditSession] datasource does not support deleting rows");
|
|
236
|
+
const selectedRowsCount = this.dataSource?.selectedRowsCount ?? 0;
|
|
237
|
+
const response = await deleteSelectedRows.call(this.dataSource, this.#deleteMode);
|
|
238
|
+
if (void 0 === response) throw Error("[EditSession] datasource returned no response when deleting rows");
|
|
239
|
+
if (isRpcError(response)) return response;
|
|
240
|
+
if (selectedRowsCount > 0) this.#setDeleteCount(this.#deleteCount + selectedRowsCount);
|
|
241
|
+
return response;
|
|
253
242
|
}
|
|
254
243
|
async addRow(rowData = {}) {
|
|
255
244
|
const addRow = this.dataSource?.addRow;
|
|
@@ -259,54 +248,50 @@ class EditSession extends EventEmitter {
|
|
|
259
248
|
...rowData
|
|
260
249
|
});
|
|
261
250
|
if (void 0 === response) throw Error("[EditSession] datasource returned no response when adding row");
|
|
262
|
-
if (!isRpcError(response)) this
|
|
251
|
+
if (!isRpcError(response)) this.#setAddCount(this.#addCount + 1);
|
|
263
252
|
return response;
|
|
264
253
|
}
|
|
265
|
-
restoreRows(keys) {
|
|
266
|
-
for (const key of keys)if (this.#deletedRows.has(key)) {
|
|
267
|
-
this.#deletedRows.delete(key);
|
|
268
|
-
this.#rowDeleteRevisions.delete(key);
|
|
269
|
-
this.deleteCount = this.#deleteCount - 1;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
254
|
hasRowChanges(key) {
|
|
273
|
-
return this.#rowEdits.has(key)
|
|
255
|
+
return this.#rowEdits.has(key);
|
|
274
256
|
}
|
|
275
257
|
isCellEdited(key, columnName) {
|
|
276
258
|
const cellEdit = this.#rowEdits.get(key)?.cellEdits.get(columnName);
|
|
277
259
|
return cellEdit?.isValid === true && cellEdit.originalValue !== cellEdit.editedValue;
|
|
278
260
|
}
|
|
279
|
-
async undoRowChange(key) {
|
|
261
|
+
async undoRowChange(key, action) {
|
|
280
262
|
if (!this.inEditMode) return;
|
|
281
263
|
const undoRevision = this.#commitRevision;
|
|
282
|
-
const deleteRevisionAtRequest = this.#rowDeleteRevisions.get(key);
|
|
283
264
|
const rowEditsAtRequest = this.#rowEdits.get(key);
|
|
284
265
|
const columnsAtRequest = new Set(rowEditsAtRequest?.cellEdits.keys());
|
|
285
|
-
const wasDeleted = this.#deletedRows.has(key);
|
|
286
266
|
const response = await this.dataSource?.undoRowChange?.(key);
|
|
287
|
-
if (
|
|
267
|
+
if (void 0 === response) return;
|
|
268
|
+
if (isRpcError(response)) return response;
|
|
269
|
+
const oldState = this.editState;
|
|
288
270
|
const rowEdits = this.#rowEdits.get(key);
|
|
289
271
|
if (rowEdits) {
|
|
290
272
|
const changedColumns = [];
|
|
273
|
+
let removedValidCount = 0;
|
|
274
|
+
let removedInvalidCount = 0;
|
|
291
275
|
for (const columnName of columnsAtRequest){
|
|
292
276
|
const latestRevision = this.#cellCommitRevisions.get(key)?.get(columnName) ?? 0;
|
|
293
|
-
if (
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
277
|
+
if (latestRevision > undoRevision) continue;
|
|
278
|
+
const existing = rowEdits.cellEdits.get(columnName);
|
|
279
|
+
if (existing) if (existing.isValid) removedValidCount++;
|
|
280
|
+
else removedInvalidCount++;
|
|
281
|
+
if (this.isCellEdited(key, columnName)) changedColumns.push(columnName);
|
|
282
|
+
rowEdits.cellEdits.delete(columnName);
|
|
283
|
+
this.#setCellCommitRevision(key, columnName);
|
|
298
284
|
}
|
|
299
285
|
if (0 === rowEdits.cellEdits.size) this.#rowEdits.delete(key);
|
|
300
|
-
this.#
|
|
286
|
+
this.#editCount -= removedValidCount;
|
|
287
|
+
this.#invalidCount -= removedInvalidCount;
|
|
301
288
|
for (const columnName of changedColumns)this.emit("cellEditChanged", key, columnName);
|
|
302
289
|
}
|
|
303
|
-
if (
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
const wasInsertedRow = response?.data?.wasInsertedRow === true;
|
|
309
|
-
if (wasInsertedRow) this.addCount = this.#addCount - 1;
|
|
290
|
+
if ("deleteRow" === action) this.#deleteCount--;
|
|
291
|
+
const wasInsertedRow = "addRow" === action || response?.data?.wasInsertedRow === true;
|
|
292
|
+
if (wasInsertedRow) this.#addCount--;
|
|
293
|
+
this.#emitEditStateChange(oldState);
|
|
294
|
+
return response;
|
|
310
295
|
}
|
|
311
296
|
#clearEdits() {
|
|
312
297
|
const oldState = this.editState;
|
|
@@ -319,8 +304,6 @@ class EditSession extends EventEmitter {
|
|
|
319
304
|
columnName
|
|
320
305
|
]));
|
|
321
306
|
this.#rowEdits.clear();
|
|
322
|
-
this.#deletedRows.clear();
|
|
323
|
-
this.#rowDeleteRevisions.clear();
|
|
324
307
|
this.#cellCommitRevisions.clear();
|
|
325
308
|
this.#editCount = 0;
|
|
326
309
|
this.#deleteCount = 0;
|
|
@@ -419,7 +402,8 @@ class EditSession extends EventEmitter {
|
|
|
419
402
|
sessionDataSource
|
|
420
403
|
});
|
|
421
404
|
try {
|
|
422
|
-
await this.dataSource?.endEditSession?.(saveChanges, force);
|
|
405
|
+
const response = await this.dataSource?.endEditSession?.(saveChanges, force);
|
|
406
|
+
if (isRpcError(response)) throw new Error(response.errorMessage);
|
|
423
407
|
this.#clearEdits();
|
|
424
408
|
this.#sessionDataSource = void 0;
|
|
425
409
|
this.#setLifecycle({
|
|
@@ -463,9 +447,24 @@ class EditSession extends EventEmitter {
|
|
|
463
447
|
editedValue,
|
|
464
448
|
isValid
|
|
465
449
|
};
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
450
|
+
let prevValidCount = 0;
|
|
451
|
+
let prevInvalidCount = 0;
|
|
452
|
+
if (existingCellEdit) if (existingCellEdit.isValid) prevValidCount = 1;
|
|
453
|
+
else prevInvalidCount = 1;
|
|
454
|
+
let nextValidCount = 0;
|
|
455
|
+
let nextInvalidCount = 0;
|
|
456
|
+
const isRevertedToOriginal = isValid && cellEdit.originalValue === editedValue;
|
|
457
|
+
if (isRevertedToOriginal) {
|
|
458
|
+
cellEdits.delete(column);
|
|
459
|
+
if (0 === cellEdits.size) this.#rowEdits.delete(key);
|
|
460
|
+
} else {
|
|
461
|
+
cellEdits.set(column, cellEdit);
|
|
462
|
+
if (isValid) nextValidCount = 1;
|
|
463
|
+
else nextInvalidCount = 1;
|
|
464
|
+
}
|
|
465
|
+
const editCountDiff = nextValidCount - prevValidCount;
|
|
466
|
+
const invalidCountDiff = nextInvalidCount - prevInvalidCount;
|
|
467
|
+
if (0 !== editCountDiff || 0 !== invalidCountDiff) this.#setEditCounts(this.#editCount + editCountDiff, this.#invalidCount + invalidCountDiff);
|
|
469
468
|
if (wasEdited !== this.isCellEdited(key, column)) this.emit("cellEditChanged", key, column);
|
|
470
469
|
return cellEdit;
|
|
471
470
|
}
|
|
@@ -479,15 +478,21 @@ class EditSession extends EventEmitter {
|
|
|
479
478
|
#isLatestCellCommit(key, columnName, revision) {
|
|
480
479
|
return this.#cellCommitRevisions.get(key)?.get(columnName) === revision;
|
|
481
480
|
}
|
|
482
|
-
async commit(key, columnName, originalValue, typedValue, isValid) {
|
|
481
|
+
async commit(key, columnName, originalValue, typedValue, isValid, { dataSourceValue, syncToDataSource = true } = {}) {
|
|
483
482
|
if ("active" !== this.#lifecycle.status && !("error" === this.#lifecycle.status && "end" === this.#lifecycle.operation)) throw new Error("No edit session in progress");
|
|
484
483
|
const revision = this.#setCellCommitRevision(key, columnName);
|
|
485
484
|
const rowEditDetails = this.#getOrCreateRowEdits(key);
|
|
486
485
|
const { cellEdits } = rowEditDetails;
|
|
487
486
|
if (isValid) {
|
|
488
487
|
const cellEdit = this.#storeCellEdit(key, cellEdits, columnName, originalValue, typedValue, isValid);
|
|
489
|
-
if (this.dataSource?.editCell) {
|
|
490
|
-
const
|
|
488
|
+
if (syncToDataSource && this.dataSource?.editCell) {
|
|
489
|
+
const valueForDataSource = dataSourceValue ?? typedValue;
|
|
490
|
+
if ("object" == typeof valueForDataSource && null !== valueForDataSource) throw new Error("Custom edit values require a scalar dataSourceValue.");
|
|
491
|
+
const response = await this.dataSource.editCell(key, columnName, valueForDataSource);
|
|
492
|
+
if (!this.inEditMode) return {
|
|
493
|
+
data: void 0,
|
|
494
|
+
type: "SUCCESS_RESULT"
|
|
495
|
+
};
|
|
491
496
|
if (!this.#isLatestCellCommit(key, columnName, revision)) throw new SupersededEditError(`Edit response superseded for ${key}:${columnName}`);
|
|
492
497
|
const currentCellEdits = this.#getOrCreateRowEdits(key).cellEdits;
|
|
493
498
|
if (isRpcError(response)) this.#storeCellEdit(key, currentCellEdits, columnName, cellEdit.originalValue, typedValue, false);
|
package/src/UndoCellRenderer.js
CHANGED
|
@@ -20,7 +20,7 @@ const UndoCellRenderer = ({ column, dataRow })=>{
|
|
|
20
20
|
children: /*#__PURE__*/ jsxs(Button, {
|
|
21
21
|
appearance: "transparent",
|
|
22
22
|
"aria-label": tooltipContent,
|
|
23
|
-
onClick: ()=>editSession?.undoRowChange(dataRow.key),
|
|
23
|
+
onClick: ()=>editSession?.undoRowChange(dataRow.key, dataRow.vuuAction),
|
|
24
24
|
style: {
|
|
25
25
|
height: "100%",
|
|
26
26
|
width: "100%"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Checkbox, Dropdown, FormField, FormFieldLabel, Input, Option } from "@salt-ds/core";
|
|
3
|
+
import { useCallback } from "react";
|
|
4
|
+
import { useEditField } from "./useEditField.js";
|
|
5
|
+
import { useEditSession } from "../DataEditingProvider.js";
|
|
6
|
+
import { dataDescriptorTypeToVuuRowDataItemType } from "@vuu-ui/vuu-utils/src/data-utils";
|
|
7
|
+
import { useEditMode } from "../EditModeProvider.js";
|
|
8
|
+
import { useLookupValues } from "../lookup-values/useLookupValues.js";
|
|
9
|
+
import "./EditField.css";
|
|
10
|
+
const classBase = "vuuEditField";
|
|
11
|
+
const EditField = ({ dataRow, deferNewRow = false, label, name, readOnly = false, required, type, ...options })=>{
|
|
12
|
+
const editSession = useEditSession();
|
|
13
|
+
const { isEditMode } = useEditMode();
|
|
14
|
+
const lookupValues = useLookupValues({
|
|
15
|
+
enabled: "dropdown" === type,
|
|
16
|
+
...options
|
|
17
|
+
});
|
|
18
|
+
const onEdit = useCallback(async (editState, editPhase)=>{
|
|
19
|
+
const { isValid = true, previousValue = "", value } = editState;
|
|
20
|
+
if ("commit" === editPhase && editSession) {
|
|
21
|
+
if (editSession.isNewRow(dataRow.key)) {
|
|
22
|
+
const isEmptyValue = "string" == typeof value && "" === value.trim();
|
|
23
|
+
if (!isValid && !isEmptyValue) return {
|
|
24
|
+
errorMessage: "Invalid value",
|
|
25
|
+
type: "ERROR_RESULT"
|
|
26
|
+
};
|
|
27
|
+
editSession.setNewRowValue(name, value);
|
|
28
|
+
if (!deferNewRow && (editSession.isNewRowFinalColumn(name) || editSession.isNewRowComplete())) return editSession.addNewRow();
|
|
29
|
+
return {
|
|
30
|
+
data: void 0,
|
|
31
|
+
type: "SUCCESS_RESULT"
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return editSession.commit(dataRow.key, name, previousValue, value, isValid);
|
|
35
|
+
}
|
|
36
|
+
if ("change" === editPhase && deferNewRow && editSession?.isNewRow(dataRow.key)) {
|
|
37
|
+
editSession.setNewRowValue(name, value);
|
|
38
|
+
return {
|
|
39
|
+
data: void 0,
|
|
40
|
+
type: "SUCCESS_RESULT"
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}, [
|
|
44
|
+
dataRow,
|
|
45
|
+
deferNewRow,
|
|
46
|
+
editSession,
|
|
47
|
+
name
|
|
48
|
+
]);
|
|
49
|
+
const onCommit = useCallback(()=>{
|
|
50
|
+
console.log("onCommit");
|
|
51
|
+
}, []);
|
|
52
|
+
const necessity = required ? "asterisk" : void 0;
|
|
53
|
+
const dataValue = dataRow?.[name] ?? "";
|
|
54
|
+
const column = {
|
|
55
|
+
label,
|
|
56
|
+
name,
|
|
57
|
+
serverDataType: "string"
|
|
58
|
+
};
|
|
59
|
+
const { editing, inputProps, onDropdownSelectionChange, onKeyDown, previousValue = "", value, warningMessage, ...editProps } = useEditField({
|
|
60
|
+
column,
|
|
61
|
+
textType: "checkbox" !== type && "dropdown" !== type ? type : void 0,
|
|
62
|
+
onEdit,
|
|
63
|
+
type: dataDescriptorTypeToVuuRowDataItemType(column),
|
|
64
|
+
value: dataValue
|
|
65
|
+
});
|
|
66
|
+
return /*#__PURE__*/ jsxs(FormField, {
|
|
67
|
+
className: classBase,
|
|
68
|
+
"data-field": name,
|
|
69
|
+
necessity: necessity,
|
|
70
|
+
children: [
|
|
71
|
+
/*#__PURE__*/ jsx(FormFieldLabel, {
|
|
72
|
+
children: label
|
|
73
|
+
}),
|
|
74
|
+
false === isEditMode ? /*#__PURE__*/ jsx(Input, {
|
|
75
|
+
bordered: true,
|
|
76
|
+
inputProps: {
|
|
77
|
+
readOnly: true
|
|
78
|
+
},
|
|
79
|
+
value: String(dataValue)
|
|
80
|
+
}) : "checkbox" === type ? /*#__PURE__*/ jsx(Checkbox, {
|
|
81
|
+
checked: Boolean(dataValue),
|
|
82
|
+
onChange: onCommit
|
|
83
|
+
}) : "dropdown" === type ? /*#__PURE__*/ jsx(Dropdown, {
|
|
84
|
+
"data-icon": "triangle-down",
|
|
85
|
+
onSelectionChange: onDropdownSelectionChange,
|
|
86
|
+
placeholder: "Please select value",
|
|
87
|
+
value: value,
|
|
88
|
+
children: lookupValues.map((listOption)=>/*#__PURE__*/ jsx(Option, {
|
|
89
|
+
value: listOption,
|
|
90
|
+
children: listOption.label
|
|
91
|
+
}, listOption.value))
|
|
92
|
+
}) : /*#__PURE__*/ jsx(Input, {
|
|
93
|
+
...editProps,
|
|
94
|
+
bordered: true,
|
|
95
|
+
onKeyDown: onKeyDown,
|
|
96
|
+
readOnly: readOnly,
|
|
97
|
+
value: value
|
|
98
|
+
})
|
|
99
|
+
]
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
export { EditField };
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { DataValidationError, dispatchCustomEvent, getTypedValue, isRpcError, isRpcSuccess } from "@vuu-ui/vuu-utils";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
const stringValueOf = (value)=>value?.toString() ?? "";
|
|
4
|
+
const useEditField = ({ column, value, onEdit, textType, type = "string" })=>{
|
|
5
|
+
const [editState, setEditState] = useState({
|
|
6
|
+
editing: false,
|
|
7
|
+
value: stringValueOf(value)
|
|
8
|
+
});
|
|
9
|
+
const initialValueRef = useRef(value?.toString() ?? "");
|
|
10
|
+
const isDirtyRef = useRef(false);
|
|
11
|
+
useEffect(()=>{
|
|
12
|
+
const nextValue = stringValueOf(value);
|
|
13
|
+
if (initialValueRef.current !== nextValue) {
|
|
14
|
+
initialValueRef.current = nextValue;
|
|
15
|
+
isDirtyRef.current = false;
|
|
16
|
+
setEditState((editState)=>({
|
|
17
|
+
...editState,
|
|
18
|
+
message: void 0,
|
|
19
|
+
value: nextValue
|
|
20
|
+
}));
|
|
21
|
+
}
|
|
22
|
+
}, [
|
|
23
|
+
value
|
|
24
|
+
]);
|
|
25
|
+
const commit = useCallback(async (value = editState.value)=>{
|
|
26
|
+
const result = column.clientSideEditValidationCheck?.(value, "*");
|
|
27
|
+
if (result?.ok === false) setEditState((state)=>({
|
|
28
|
+
...state,
|
|
29
|
+
message: result?.messages?.join(",")
|
|
30
|
+
}));
|
|
31
|
+
else {
|
|
32
|
+
const { current: initialValue } = initialValueRef;
|
|
33
|
+
const previousValue = getTypedValue(initialValue, type);
|
|
34
|
+
try {
|
|
35
|
+
const typedValue = getTypedValue(value, type, true);
|
|
36
|
+
const response = await onEdit?.({
|
|
37
|
+
editType: "commit",
|
|
38
|
+
isValid: true,
|
|
39
|
+
previousValue,
|
|
40
|
+
value: typedValue
|
|
41
|
+
}, "commit");
|
|
42
|
+
if (isRpcSuccess(response)) {
|
|
43
|
+
isDirtyRef.current = false;
|
|
44
|
+
setEditState((state)=>({
|
|
45
|
+
editing: false,
|
|
46
|
+
previousValue: state.previousValue === value ? void 0 : void 0 === state.previousValue ? initialValue : state.previousValue,
|
|
47
|
+
value
|
|
48
|
+
}));
|
|
49
|
+
} else if (isRpcError(response)) {
|
|
50
|
+
setEditState((state)=>({
|
|
51
|
+
...state,
|
|
52
|
+
message: response.errorMessage
|
|
53
|
+
}));
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
initialValueRef.current = value;
|
|
57
|
+
return true;
|
|
58
|
+
} catch (e) {
|
|
59
|
+
if (e instanceof DataValidationError) {
|
|
60
|
+
const { actualType, expectedType, message } = e;
|
|
61
|
+
if (column) {
|
|
62
|
+
const { name, label = name } = column;
|
|
63
|
+
const message = `${label} is a ${expectedType} value, data entered is ${actualType} `;
|
|
64
|
+
setEditState((state)=>({
|
|
65
|
+
...state,
|
|
66
|
+
message
|
|
67
|
+
}));
|
|
68
|
+
onEdit?.({
|
|
69
|
+
editType: "commit",
|
|
70
|
+
isValid: false,
|
|
71
|
+
previousValue,
|
|
72
|
+
value
|
|
73
|
+
}, "commit");
|
|
74
|
+
} else setEditState((state)=>({
|
|
75
|
+
...state,
|
|
76
|
+
message
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}, [
|
|
83
|
+
column,
|
|
84
|
+
editState,
|
|
85
|
+
onEdit,
|
|
86
|
+
type
|
|
87
|
+
]);
|
|
88
|
+
const toggleActivation = useCallback(async (input, cancel = false)=>{
|
|
89
|
+
if (editState.editing) if (isDirtyRef.current) if (cancel) {
|
|
90
|
+
const { value: previousValue } = editState;
|
|
91
|
+
isDirtyRef.current = false;
|
|
92
|
+
setEditState({
|
|
93
|
+
editing: false,
|
|
94
|
+
value: initialValueRef.current,
|
|
95
|
+
message: void 0
|
|
96
|
+
});
|
|
97
|
+
onEdit?.({
|
|
98
|
+
editType: "cancel",
|
|
99
|
+
isValid: true,
|
|
100
|
+
previousValue,
|
|
101
|
+
value: getTypedValue(initialValueRef.current, type, true)
|
|
102
|
+
}, "commit");
|
|
103
|
+
} else {
|
|
104
|
+
const commitSuccessful = await commit();
|
|
105
|
+
if (commitSuccessful) {
|
|
106
|
+
setEditState((editState)=>({
|
|
107
|
+
...editState,
|
|
108
|
+
editing: false
|
|
109
|
+
}));
|
|
110
|
+
dispatchCustomEvent(input, "vuu-commit");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
else setEditState((editState)=>({
|
|
114
|
+
...editState,
|
|
115
|
+
editing: false
|
|
116
|
+
}));
|
|
117
|
+
else if (!cancel) {
|
|
118
|
+
setEditState((editState)=>({
|
|
119
|
+
...editState,
|
|
120
|
+
editing: true
|
|
121
|
+
}));
|
|
122
|
+
input.select();
|
|
123
|
+
}
|
|
124
|
+
}, [
|
|
125
|
+
commit,
|
|
126
|
+
editState,
|
|
127
|
+
onEdit,
|
|
128
|
+
type
|
|
129
|
+
]);
|
|
130
|
+
const handleKeyDown = useCallback(async (evt)=>{
|
|
131
|
+
const { key, target } = evt;
|
|
132
|
+
const input = target;
|
|
133
|
+
if ("Enter" === key) toggleActivation(input);
|
|
134
|
+
else if ("ArrowRight" === key || "ArrowLeft" === key || "ArrowUp" === key || "ArrowDown" === key) {
|
|
135
|
+
if (editState.editing) evt.stopPropagation();
|
|
136
|
+
} else if ("Escape" === evt.key) toggleActivation(input, true);
|
|
137
|
+
}, [
|
|
138
|
+
editState,
|
|
139
|
+
toggleActivation
|
|
140
|
+
]);
|
|
141
|
+
const handleFocus = useCallback((e)=>{
|
|
142
|
+
const input = e.target;
|
|
143
|
+
const source = e.relatedTarget;
|
|
144
|
+
if (source?.classList.contains("vuuTableCell") && source.contains(input)) toggleActivation(input);
|
|
145
|
+
}, [
|
|
146
|
+
toggleActivation
|
|
147
|
+
]);
|
|
148
|
+
const handleBlur = useCallback(async ()=>{
|
|
149
|
+
if (editState.editing) {
|
|
150
|
+
if (isDirtyRef.current) {
|
|
151
|
+
const commitSuccessful = await commit();
|
|
152
|
+
console.log({
|
|
153
|
+
commitSuccessful
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
setEditState((editState)=>({
|
|
157
|
+
...editState,
|
|
158
|
+
editing: false
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
}, [
|
|
162
|
+
commit,
|
|
163
|
+
editState
|
|
164
|
+
]);
|
|
165
|
+
const handleChange = useCallback((evt)=>{
|
|
166
|
+
const { value } = evt.target;
|
|
167
|
+
isDirtyRef.current = value !== initialValueRef.current;
|
|
168
|
+
const result = column.clientSideEditValidationCheck?.(value, "change");
|
|
169
|
+
setEditState({
|
|
170
|
+
editing: true,
|
|
171
|
+
value
|
|
172
|
+
});
|
|
173
|
+
onEdit?.({
|
|
174
|
+
editType: "change",
|
|
175
|
+
isValid: result?.ok !== false,
|
|
176
|
+
previousValue: initialValueRef.current,
|
|
177
|
+
value
|
|
178
|
+
}, "change");
|
|
179
|
+
if (result?.ok === false) setEditState({
|
|
180
|
+
editing: true,
|
|
181
|
+
value,
|
|
182
|
+
message: result.messages?.join(",")
|
|
183
|
+
});
|
|
184
|
+
}, [
|
|
185
|
+
column,
|
|
186
|
+
onEdit
|
|
187
|
+
]);
|
|
188
|
+
const handleDropdownSelectionChange = useCallback((_evt, [selectedOption])=>{
|
|
189
|
+
if (!selectedOption) return;
|
|
190
|
+
const value = String(selectedOption.value);
|
|
191
|
+
isDirtyRef.current = value !== initialValueRef.current;
|
|
192
|
+
setEditState((state)=>({
|
|
193
|
+
...state,
|
|
194
|
+
editing: true,
|
|
195
|
+
message: void 0,
|
|
196
|
+
value
|
|
197
|
+
}));
|
|
198
|
+
commit(value).then((committed)=>{
|
|
199
|
+
if (committed) setEditState((state)=>({
|
|
200
|
+
...state,
|
|
201
|
+
editing: false,
|
|
202
|
+
value
|
|
203
|
+
}));
|
|
204
|
+
});
|
|
205
|
+
}, [
|
|
206
|
+
commit
|
|
207
|
+
]);
|
|
208
|
+
return {
|
|
209
|
+
editing: editState.editing,
|
|
210
|
+
inputProps: {
|
|
211
|
+
onBlur: handleBlur,
|
|
212
|
+
onFocus: handleFocus,
|
|
213
|
+
type: textType
|
|
214
|
+
},
|
|
215
|
+
onKeyDown: handleKeyDown,
|
|
216
|
+
previousValue: editState.previousValue,
|
|
217
|
+
value: editState.value,
|
|
218
|
+
warningMessage: editState.message,
|
|
219
|
+
onChange: handleChange,
|
|
220
|
+
onDropdownSelectionChange: handleDropdownSelectionChange
|
|
221
|
+
};
|
|
222
|
+
};
|
|
223
|
+
export { useEditField };
|
package/src/index.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
export { StaleUpdateError } from "@vuu-ui/vuu-utils";
|
|
1
2
|
export { DataEditingProvider, useEditSession } from "./DataEditingProvider.js";
|
|
2
|
-
export {
|
|
3
|
+
export { EditField } from "./edit-field/EditField.js";
|
|
4
|
+
export { useEditField } from "./edit-field/useEditField.js";
|
|
5
|
+
export { lookupOptionsFromRows, useLookupValues } from "./lookup-values/useLookupValues.js";
|
|
3
6
|
export { getVuuEditMessage, isEditRowReadOnly, isInlineEditingSession, withDataRowEditErrors } from "./edit-utils.js";
|
|
7
|
+
export { EDIT_ACTION_ROW_CLASS_NAME_GENERATOR, editActionRowClassNameGenerator } from "./editActionRowClassNameGenerator.js";
|
|
4
8
|
export { EditButtons } from "./EditButtons.js";
|
|
5
9
|
export { EditModeProvider, useEditMode } from "./EditModeProvider.js";
|
|
6
10
|
export { EditError, EditSession, SupersededEditError } from "./EditSession.js";
|
|
7
|
-
export { StaleUpdateError } from "@vuu-ui/vuu-utils";
|
|
8
|
-
export { EDIT_ACTION_ROW_CLASS_NAME_GENERATOR, editActionRowClassNameGenerator } from "./editActionRowClassNameGenerator.js";
|
|
9
11
|
export { UNDO_CELL_RENDERER, UndoCellRenderer, getUndoButtonContent, getUndoTooltipContent } from "./UndoCellRenderer.js";
|
|
12
|
+
export { useCellEdited } from "./useCellEdited.js";
|
|
13
|
+
export { useEditable } from "./useEditable.js";
|
|
10
14
|
export { useEditableTable } from "./useEditableTable.js";
|
|
15
|
+
export { useEditState } from "./useEditState.js";
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Range, useData } from "@vuu-ui/vuu-utils";
|
|
2
|
+
import { useEffect, useMemo, useState } from "react";
|
|
3
|
+
const DATA_ROW_COLUMN_OFFSET = 10;
|
|
4
|
+
const lookupOptionsFromRows = (columns, rows, optionMap)=>{
|
|
5
|
+
const requestedColumns = [
|
|
6
|
+
optionMap.value,
|
|
7
|
+
optionMap.label,
|
|
8
|
+
...optionMap.additionalFields ?? []
|
|
9
|
+
];
|
|
10
|
+
const missingColumns = requestedColumns.filter((column)=>!columns.includes(column));
|
|
11
|
+
if (missingColumns.length > 0) throw Error(`[useLookupValues] lookup columns not found: ${requestedColumns.join(", ")}`);
|
|
12
|
+
return rows.map((row)=>{
|
|
13
|
+
const metadata = Object.fromEntries((optionMap.additionalFields ?? []).map((field)=>[
|
|
14
|
+
field,
|
|
15
|
+
String(row[columns.indexOf(field) + DATA_ROW_COLUMN_OFFSET])
|
|
16
|
+
]));
|
|
17
|
+
return {
|
|
18
|
+
value: String(row[columns.indexOf(optionMap.value) + DATA_ROW_COLUMN_OFFSET]),
|
|
19
|
+
label: String(row[columns.indexOf(optionMap.label) + DATA_ROW_COLUMN_OFFSET]),
|
|
20
|
+
...Object.keys(metadata).length > 0 ? {
|
|
21
|
+
metadata
|
|
22
|
+
} : {}
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
const useLookupValues = ({ enabled, optionMap, table })=>{
|
|
27
|
+
if (enabled && (void 0 === optionMap || void 0 === table)) throw Error("[useLookupValues] optionsMap and Table must be provided");
|
|
28
|
+
const { VuuDataSource } = useData();
|
|
29
|
+
const [options, setOptions] = useState([]);
|
|
30
|
+
const lookupColumns = useMemo(()=>optionMap ? [
|
|
31
|
+
optionMap.value,
|
|
32
|
+
optionMap.label,
|
|
33
|
+
...optionMap.additionalFields ?? []
|
|
34
|
+
] : [], [
|
|
35
|
+
optionMap
|
|
36
|
+
]);
|
|
37
|
+
const dataSource = useMemo(()=>enabled && optionMap && table ? new VuuDataSource({
|
|
38
|
+
columns: lookupColumns,
|
|
39
|
+
table
|
|
40
|
+
}) : void 0, [
|
|
41
|
+
VuuDataSource,
|
|
42
|
+
enabled,
|
|
43
|
+
lookupColumns,
|
|
44
|
+
optionMap,
|
|
45
|
+
table
|
|
46
|
+
]);
|
|
47
|
+
useEffect(()=>{
|
|
48
|
+
setOptions([]);
|
|
49
|
+
if (!dataSource || !optionMap) return;
|
|
50
|
+
let active = true;
|
|
51
|
+
let columns;
|
|
52
|
+
const subscribe = (message)=>{
|
|
53
|
+
if (!active) return;
|
|
54
|
+
if ("subscribed" === message.type) {
|
|
55
|
+
const schemaColumns = new Set(message.tableSchema.columns.map(({ name })=>name));
|
|
56
|
+
if (!lookupColumns.every((column)=>message.columns.includes(column) && schemaColumns.has(column))) throw Error(`[useLookupValues] lookup columns not found: ${lookupColumns.join(", ")}`);
|
|
57
|
+
columns = message.columns;
|
|
58
|
+
} else if ("viewport-update" === message.type && columns) setOptions(lookupOptionsFromRows(columns, message.rows ?? [], optionMap));
|
|
59
|
+
else if ("subscribe-failed" === message.type) console.error(`Lookup table ${table?.table} subscription failed: ${message.msg}`);
|
|
60
|
+
};
|
|
61
|
+
dataSource.subscribe({
|
|
62
|
+
range: Range(0, 100)
|
|
63
|
+
}, subscribe).catch((cause)=>{
|
|
64
|
+
if (active) console.error("Role client dropdown subscription failed:", cause);
|
|
65
|
+
});
|
|
66
|
+
return ()=>{
|
|
67
|
+
active = false;
|
|
68
|
+
dataSource.unsubscribe();
|
|
69
|
+
};
|
|
70
|
+
}, [
|
|
71
|
+
dataSource,
|
|
72
|
+
lookupColumns,
|
|
73
|
+
optionMap,
|
|
74
|
+
table
|
|
75
|
+
]);
|
|
76
|
+
return options;
|
|
77
|
+
};
|
|
78
|
+
export { lookupOptionsFromRows, useLookupValues };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useCallback, useSyncExternalStore } from "react";
|
|
2
|
+
function useEditState(editSession) {
|
|
3
|
+
const subscribe = useCallback((onStoreChange)=>{
|
|
4
|
+
if (!editSession) return ()=>void 0;
|
|
5
|
+
editSession.on("editState", onStoreChange);
|
|
6
|
+
return ()=>editSession.removeListener("editState", onStoreChange);
|
|
7
|
+
}, [
|
|
8
|
+
editSession
|
|
9
|
+
]);
|
|
10
|
+
const getSnapshot = useCallback(()=>editSession?.editState ?? "clean", [
|
|
11
|
+
editSession
|
|
12
|
+
]);
|
|
13
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
14
|
+
}
|
|
15
|
+
export { useEditState };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { EditSession, useEditMode } from "./index.js";
|
|
2
|
+
import { useCallback, useEffect, useMemo } from "react";
|
|
3
|
+
const useEditable = ({ copyOption = "Selected", dataSource, onCancel, onSave })=>{
|
|
4
|
+
const { isEditMode } = useEditMode();
|
|
5
|
+
const editSession = useMemo(()=>new EditSession({
|
|
6
|
+
dataSource
|
|
7
|
+
}), [
|
|
8
|
+
dataSource
|
|
9
|
+
]);
|
|
10
|
+
const handleCancel = useCallback(async ()=>{
|
|
11
|
+
try {
|
|
12
|
+
await editSession.end();
|
|
13
|
+
onCancel();
|
|
14
|
+
} catch (error) {
|
|
15
|
+
console.error("[useEditableTable] cancel edit session failed", error);
|
|
16
|
+
}
|
|
17
|
+
}, [
|
|
18
|
+
editSession,
|
|
19
|
+
onCancel
|
|
20
|
+
]);
|
|
21
|
+
const handleSave = useCallback(async (force = false)=>{
|
|
22
|
+
try {
|
|
23
|
+
await editSession.end(true, force);
|
|
24
|
+
onSave();
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.error("[useEditableTable] save edit session failed", error);
|
|
27
|
+
}
|
|
28
|
+
}, [
|
|
29
|
+
editSession,
|
|
30
|
+
onSave
|
|
31
|
+
]);
|
|
32
|
+
useEffect(()=>{
|
|
33
|
+
const transition = isEditMode ? editSession.begin(copyOption) : editSession.end();
|
|
34
|
+
transition.catch((error)=>{
|
|
35
|
+
if (isEditMode) {
|
|
36
|
+
console.error("[useEditableTable] begin edit session failed", error);
|
|
37
|
+
handleCancel();
|
|
38
|
+
} else console.error("[useEditableTable] end edit session failed", error);
|
|
39
|
+
});
|
|
40
|
+
}, [
|
|
41
|
+
copyOption,
|
|
42
|
+
editSession,
|
|
43
|
+
isEditMode,
|
|
44
|
+
handleCancel
|
|
45
|
+
]);
|
|
46
|
+
return {
|
|
47
|
+
editSession,
|
|
48
|
+
onCancel: handleCancel,
|
|
49
|
+
onSave: handleSave
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
export { useEditable };
|
package/src/useEditableTable.js
CHANGED
|
@@ -74,7 +74,7 @@ const useEditableTable = ({ columns, dataSource: dataSourceProp, deleteMode = "s
|
|
|
74
74
|
}, [
|
|
75
75
|
editSession
|
|
76
76
|
]);
|
|
77
|
-
const handleUndoRowChange = useCallback((key)=>void editSession.undoRowChange(key), [
|
|
77
|
+
const handleUndoRowChange = useCallback((key, action)=>void editSession.undoRowChange(key, action), [
|
|
78
78
|
editSession
|
|
79
79
|
]);
|
|
80
80
|
useEffect(()=>{
|
package/types/EditButtons.d.ts
CHANGED
|
@@ -11,4 +11,4 @@ export interface EditButtonProps {
|
|
|
11
11
|
confirmSave?: () => boolean | Promise<boolean>;
|
|
12
12
|
confirmCancel?: () => boolean | Promise<boolean>;
|
|
13
13
|
}
|
|
14
|
-
export declare const EditButtons: ({ canCancel, canSave, confirmCancel, confirmSave, editSession, hasSelection, onCancel, onDelete, onSave, saveLabel, }: EditButtonProps) => import("react").JSX.Element;
|
|
14
|
+
export declare const EditButtons: ({ canCancel, canSave: _canSave, confirmCancel, confirmSave, editSession, hasSelection, onCancel, onDelete, onSave, saveLabel, }: EditButtonProps) => import("react").JSX.Element;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ReactNode } from "react";
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
2
|
export interface EditModeContextProps {
|
|
3
3
|
isEditMode: boolean;
|
|
4
4
|
setEditMode: (inEditMode: boolean) => void;
|
|
@@ -7,7 +7,8 @@ export interface EditModeContextProps {
|
|
|
7
7
|
* Implemented as a standalone Provider so that EditMode cna be implemented
|
|
8
8
|
* at higher level than individual edit controls.
|
|
9
9
|
*/
|
|
10
|
-
export declare const EditModeProvider: ({ children }: {
|
|
10
|
+
export declare const EditModeProvider: ({ children, isEditMode: isEditModeProp, }: {
|
|
11
11
|
children: ReactNode;
|
|
12
|
+
isEditMode?: boolean;
|
|
12
13
|
}) => import("react").JSX.Element;
|
|
13
14
|
export declare const useEditMode: () => EditModeContextProps;
|
package/types/EditSession.d.ts
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
import type { CopyOption, DataSource, DeleteRowMode,
|
|
1
|
+
import type { CopyOption, DataSource, DeleteRowMode, SessionType } from "@vuu-ui/vuu-data-types";
|
|
2
2
|
import type { RpcResult, VuuRowDataItemType } from "@vuu-ui/vuu-protocol-types";
|
|
3
3
|
import { EventEmitter } from "@vuu-ui/vuu-utils";
|
|
4
4
|
export type EditState = "clean" | "dirty" | "invalid" | "stale";
|
|
5
|
+
export type EditActionType = "deleteRow" | "addRow" | "editCell";
|
|
6
|
+
export type EditSessionValue = VuuRowDataItemType | object;
|
|
7
|
+
export type EditCommitOptions = {
|
|
8
|
+
dataSourceValue?: VuuRowDataItemType;
|
|
9
|
+
syncToDataSource?: boolean;
|
|
10
|
+
};
|
|
5
11
|
/** Column name to default value mapping applied to every addRow call when a column is absent from the row data. */
|
|
6
12
|
export type RowDefaultDataItemValues = Record<string, VuuRowDataItemType>;
|
|
7
13
|
export type EditSessionApi = "createSessionDataSource" | "beginEditSession";
|
|
8
14
|
export type EditSessionConstructorProps = {
|
|
9
|
-
dataSource:
|
|
15
|
+
dataSource: DataSource;
|
|
10
16
|
/** @default "soft" */
|
|
11
17
|
deleteMode?: DeleteRowMode;
|
|
12
18
|
/** @default "createSessionDataSource" */
|
|
@@ -53,14 +59,13 @@ export declare class EditSession extends EventEmitter<EditSessionEvents> {
|
|
|
53
59
|
static readonly newRowKey = "__vuu_new_row__";
|
|
54
60
|
constructor({ dataSource, deleteMode, editSessionApi, rowDefaults, }: EditSessionConstructorProps);
|
|
55
61
|
get editCount(): number;
|
|
56
|
-
set editCount(val: number);
|
|
57
62
|
get invalidCount(): number;
|
|
58
|
-
set invalidCount(val: number);
|
|
59
63
|
get deleteCount(): number;
|
|
60
|
-
set deleteCount(val: number);
|
|
61
64
|
get addCount(): number;
|
|
62
|
-
set addCount(val: number);
|
|
63
65
|
get editState(): EditState;
|
|
66
|
+
get isDirty(): boolean;
|
|
67
|
+
get canCancel(): boolean;
|
|
68
|
+
get canSave(): boolean;
|
|
64
69
|
get newRowState(): NewRowState;
|
|
65
70
|
isNewRow(key: string): key is "__vuu_new_row__";
|
|
66
71
|
isNewRowFinalColumn(columnName: string): boolean;
|
|
@@ -68,23 +73,22 @@ export declare class EditSession extends EventEmitter<EditSessionEvents> {
|
|
|
68
73
|
configureNewRow(columns: readonly string[], requiredColumns?: readonly string[]): void;
|
|
69
74
|
setNewRowValue(column: string, value: VuuRowDataItemType): void;
|
|
70
75
|
addNewRow(): Promise<RpcResult>;
|
|
71
|
-
deleteSelectedRows(): Promise<
|
|
76
|
+
deleteSelectedRows(): Promise<RpcResult>;
|
|
72
77
|
addRow(rowData?: Record<string, VuuRowDataItemType>): Promise<RpcResult>;
|
|
73
|
-
restoreRows(keys: string[]): void;
|
|
74
78
|
hasRowChanges(key: string): boolean;
|
|
75
79
|
isCellEdited(key: string, columnName: string): boolean;
|
|
76
|
-
undoRowChange(key: string): Promise<
|
|
80
|
+
undoRowChange(key: string, action: EditActionType): Promise<RpcResult | undefined>;
|
|
77
81
|
begin(copyOption?: CopyOption, sessionType?: SessionType): Promise<DataSource>;
|
|
78
82
|
/**
|
|
79
83
|
* Safe to call repeatedly - schema of a remote session table typically arrives
|
|
80
84
|
* after begin() resolves, so callers re-invoke this once 'subscribed' fires.
|
|
81
85
|
*/
|
|
82
86
|
reconcileWithSessionSchema(): void;
|
|
83
|
-
get dataSource():
|
|
87
|
+
get dataSource(): import("@vuu-ui/vuu-data-types").DataSourceBase<import("@vuu-ui/vuu-data-types").DataSourceRowWithBigint> | undefined;
|
|
84
88
|
get sessionDataSource(): DataSource | undefined;
|
|
85
89
|
end(saveChanges?: boolean, force?: boolean): Promise<void>;
|
|
86
90
|
get lifecycle(): EditLifecycle;
|
|
87
91
|
get inEditMode(): boolean;
|
|
88
|
-
commit(key: string, columnName: string, originalValue:
|
|
92
|
+
commit(key: string, columnName: string, originalValue: EditSessionValue, typedValue: EditSessionValue, isValid: boolean, { dataSourceValue, syncToDataSource }?: EditCommitOptions): Promise<RpcResult>;
|
|
89
93
|
}
|
|
90
94
|
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { DataRow } from "@vuu-ui/vuu-table-types";
|
|
2
|
+
import type { VuuTable } from "@vuu-ui/vuu-protocol-types";
|
|
3
|
+
import { type OptionMap } from "../lookup-values/useLookupValues";
|
|
4
|
+
import "./EditField.css";
|
|
5
|
+
export type TextFieldType = "email" | "password";
|
|
6
|
+
export type EditFieldType = "checkbox" | "dropdown" | TextFieldType;
|
|
7
|
+
export interface EditFieldProps {
|
|
8
|
+
dataRow: DataRow;
|
|
9
|
+
deferNewRow?: boolean;
|
|
10
|
+
label: string;
|
|
11
|
+
name: string;
|
|
12
|
+
readOnly?: boolean;
|
|
13
|
+
required?: boolean;
|
|
14
|
+
type?: EditFieldType;
|
|
15
|
+
}
|
|
16
|
+
export interface DropdownEditFieldProps extends EditFieldProps {
|
|
17
|
+
type: "dropdown";
|
|
18
|
+
optionMap: OptionMap;
|
|
19
|
+
table: VuuTable;
|
|
20
|
+
}
|
|
21
|
+
export declare const EditField: ({ dataRow, deferNewRow, label, name, readOnly, required, type, ...options }: EditFieldProps | DropdownEditFieldProps) => import("react").JSX.Element;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { VuuRowDataItemType } from "@vuu-ui/vuu-protocol-types";
|
|
2
|
+
import type { RuntimeColumnDescriptor, TableCellEditHandler } from "@vuu-ui/vuu-table-types";
|
|
3
|
+
import { type ChangeEventHandler, type FocusEventHandler, type KeyboardEvent, type SyntheticEvent } from "react";
|
|
4
|
+
import type { LookupOption } from "../lookup-values/useLookupValues";
|
|
5
|
+
import type { TextFieldType } from "./EditField";
|
|
6
|
+
export interface EditFieldHookProps<T extends VuuRowDataItemType = VuuRowDataItemType> {
|
|
7
|
+
column: Pick<RuntimeColumnDescriptor, "clientSideEditValidationCheck" | "label" | "name">;
|
|
8
|
+
value?: T;
|
|
9
|
+
onEdit?: TableCellEditHandler;
|
|
10
|
+
textType?: TextFieldType;
|
|
11
|
+
type?: "string" | "number" | "boolean";
|
|
12
|
+
}
|
|
13
|
+
export declare const useEditField: <T extends string | number | boolean = string>({ column, value, onEdit, textType, type, }: EditFieldHookProps<T>) => {
|
|
14
|
+
editing: boolean;
|
|
15
|
+
inputProps: {
|
|
16
|
+
onBlur: FocusEventHandler<HTMLElement>;
|
|
17
|
+
onFocus: FocusEventHandler<HTMLElement>;
|
|
18
|
+
type: TextFieldType | undefined;
|
|
19
|
+
};
|
|
20
|
+
onKeyDown: (evt: KeyboardEvent<HTMLElement>) => Promise<void>;
|
|
21
|
+
previousValue: string | undefined;
|
|
22
|
+
value: string;
|
|
23
|
+
warningMessage: string | undefined;
|
|
24
|
+
onChange: ChangeEventHandler;
|
|
25
|
+
onDropdownSelectionChange: (_evt: SyntheticEvent, [selectedOption]: LookupOption[]) => void;
|
|
26
|
+
};
|
package/types/index.d.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
export { StaleUpdateError } from "@vuu-ui/vuu-utils";
|
|
1
2
|
export { DataEditingProvider, useEditSession } from "./DataEditingProvider";
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
3
|
+
export { EditField } from './edit-field/EditField';
|
|
4
|
+
export { useEditField } from './edit-field/useEditField';
|
|
5
|
+
export { lookupOptionsFromRows, useLookupValues, type LookupOption, type LookupValuedHookProps, type OptionMap, } from "./lookup-values/useLookupValues";
|
|
6
|
+
export { getVuuEditMessage, isEditRowReadOnly, isInlineEditingSession, withDataRowEditErrors } from "./edit-utils";
|
|
7
|
+
export { EDIT_ACTION_ROW_CLASS_NAME_GENERATOR, editActionRowClassNameGenerator } from "./editActionRowClassNameGenerator";
|
|
4
8
|
export { EditButtons, type EditButtonProps } from "./EditButtons";
|
|
5
|
-
export { EditModeProvider, useEditMode, type EditModeContextProps
|
|
6
|
-
export { EditError, EditSession, SupersededEditError, type EditLifecycle, type EditSessionConstructorProps, type
|
|
7
|
-
export {
|
|
8
|
-
export {
|
|
9
|
-
export {
|
|
10
|
-
export { useEditableTable, type EditableTableHookProps, type EditMode
|
|
9
|
+
export { EditModeProvider, useEditMode, type EditModeContextProps } from "./EditModeProvider";
|
|
10
|
+
export { EditError, EditSession, SupersededEditError, type EditActionType, type EditLifecycle, type EditSessionConstructorProps, type EditState, type NewRowState, type RowDefaultDataItemValues } from "./EditSession";
|
|
11
|
+
export { getUndoButtonContent, getUndoTooltipContent, UNDO_CELL_RENDERER, UndoCellRenderer, type UndoCellRendererComponentProps } from "./UndoCellRenderer";
|
|
12
|
+
export { useCellEdited } from "./useCellEdited";
|
|
13
|
+
export { useEditable } from './useEditable';
|
|
14
|
+
export { useEditableTable, type EditableTableHookProps, type EditMode } from "./useEditableTable";
|
|
15
|
+
export { useEditState } from "./useEditState";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { DataSourceRow } from "@vuu-ui/vuu-data-types";
|
|
2
|
+
import type { VuuTable } from "@vuu-ui/vuu-protocol-types";
|
|
3
|
+
export type LookupOption = {
|
|
4
|
+
label: string;
|
|
5
|
+
metadata?: Record<string, string>;
|
|
6
|
+
value: number | string;
|
|
7
|
+
};
|
|
8
|
+
export type OptionMap = {
|
|
9
|
+
additionalFields?: readonly string[];
|
|
10
|
+
label: string;
|
|
11
|
+
value: string;
|
|
12
|
+
};
|
|
13
|
+
export interface LookupValuedHookProps {
|
|
14
|
+
enabled: boolean;
|
|
15
|
+
optionMap?: OptionMap;
|
|
16
|
+
table?: VuuTable;
|
|
17
|
+
}
|
|
18
|
+
export declare const lookupOptionsFromRows: (columns: string[], rows: DataSourceRow[], optionMap: OptionMap) => LookupOption[];
|
|
19
|
+
export declare const useLookupValues: ({ enabled, optionMap, table, }: LookupValuedHookProps) => LookupOption[];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { CopyOption, DataSource } from "@vuu-ui/vuu-data-types";
|
|
2
|
+
import { EditSession } from ".";
|
|
3
|
+
export interface EditableHookProps {
|
|
4
|
+
copyOption?: CopyOption;
|
|
5
|
+
dataSource: DataSource;
|
|
6
|
+
onCancel: () => void;
|
|
7
|
+
onSave: () => void;
|
|
8
|
+
}
|
|
9
|
+
export declare const useEditable: ({ copyOption, dataSource, onCancel, onSave }: EditableHookProps) => {
|
|
10
|
+
editSession: EditSession;
|
|
11
|
+
onCancel: () => Promise<void>;
|
|
12
|
+
onSave: (force?: boolean) => Promise<void>;
|
|
13
|
+
};
|
|
@@ -1,6 +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 { EditSession, type EditLifecycle, type EditSessionApi, type RowDefaultDataItemValues } from "./EditSession";
|
|
3
|
+
import { EditSession, type EditActionType, type EditLifecycle, type EditSessionApi, type RowDefaultDataItemValues } from "./EditSession";
|
|
4
4
|
export type EditMode = "edit" | "view";
|
|
5
5
|
export interface EditableTableHookProps {
|
|
6
6
|
/**
|
|
@@ -37,7 +37,7 @@ export declare const useEditableTable: ({ columns, dataSource: dataSourceProp, d
|
|
|
37
37
|
onCancel: () => Promise<void>;
|
|
38
38
|
onDelete: () => Promise<void>;
|
|
39
39
|
onSave: (force?: boolean) => Promise<void>;
|
|
40
|
-
onUndoRowChange: (key: string) => undefined;
|
|
40
|
+
onUndoRowChange: (key: string, action: EditActionType) => undefined;
|
|
41
41
|
rowClassNameGenerators: string[] | undefined;
|
|
42
42
|
sessionDataSource: import("@vuu-ui/vuu-data-types").DataSourceBase<import("@vuu-ui/vuu-data-types").DataSourceRowWithBigint> | undefined;
|
|
43
43
|
sourceDataSource: DataSource;
|