@vuu-ui/vuu-data-editing 3.0.0 → 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.
- package/package.json +15 -12
- package/src/DataEditingProvider.js +13 -0
- package/src/EditButtons.js +58 -0
- package/src/EditModeProvider.js +18 -0
- package/src/EditSession.js +482 -0
- package/src/UndoCellRenderer.js +38 -0
- package/src/edit-utils.js +47 -0
- package/src/editActionRowClassNameGenerator.js +15 -0
- package/src/index.js +10 -0
- package/src/useCellEdited.js +22 -0
- package/src/useEditableTable.js +141 -0
- package/cjs/DataEditingProvider.js +0 -28
- package/cjs/DataEditingProvider.js.map +0 -1
- package/cjs/EditButtons.js +0 -59
- package/cjs/EditButtons.js.map +0 -1
- package/cjs/EditModeProvider.js +0 -18
- package/cjs/EditModeProvider.js.map +0 -1
- package/cjs/EditSession.js +0 -564
- package/cjs/EditSession.js.map +0 -1
- package/cjs/UndoCellRenderer.js +0 -48
- package/cjs/UndoCellRenderer.js.map +0 -1
- package/cjs/edit-utils.js +0 -52
- package/cjs/edit-utils.js.map +0 -1
- package/cjs/editActionRowClassNameGenerator.js +0 -25
- package/cjs/editActionRowClassNameGenerator.js.map +0 -1
- package/cjs/index.js +0 -43
- package/cjs/index.js.map +0 -1
- package/cjs/useCellEdited.js +0 -29
- package/cjs/useCellEdited.js.map +0 -1
- package/cjs/useEditableTable.js +0 -145
- package/cjs/useEditableTable.js.map +0 -1
- package/esm/DataEditingProvider.js +0 -25
- package/esm/DataEditingProvider.js.map +0 -1
- package/esm/EditButtons.js +0 -57
- package/esm/EditButtons.js.map +0 -1
- package/esm/EditModeProvider.js +0 -15
- package/esm/EditModeProvider.js.map +0 -1
- package/esm/EditSession.js +0 -560
- package/esm/EditSession.js.map +0 -1
- package/esm/UndoCellRenderer.js +0 -43
- package/esm/UndoCellRenderer.js.map +0 -1
- package/esm/edit-utils.js +0 -45
- package/esm/edit-utils.js.map +0 -1
- package/esm/editActionRowClassNameGenerator.js +0 -22
- package/esm/editActionRowClassNameGenerator.js.map +0 -1
- package/esm/index.js +0 -11
- package/esm/index.js.map +0 -1
- package/esm/useCellEdited.js +0 -27
- package/esm/useCellEdited.js.map +0 -1
- package/esm/useEditableTable.js +0 -143
- package/esm/useEditableTable.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "3.0.
|
|
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.
|
|
10
|
-
"@vuu-ui/vuu-utils": "3.0.
|
|
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.
|
|
14
|
-
"@vuu-ui/vuu-protocol-types": "3.0.
|
|
15
|
-
"@vuu-ui/vuu-table-types": "3.0.
|
|
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
|
-
"
|
|
24
|
-
"cjs",
|
|
29
|
+
"src",
|
|
25
30
|
"/types"
|
|
26
31
|
],
|
|
27
32
|
"exports": {
|
|
28
33
|
".": {
|
|
29
|
-
"
|
|
30
|
-
"import": "./esm/index.js",
|
|
34
|
+
"import": "./src/index.js",
|
|
31
35
|
"types": "./types/index.d.ts"
|
|
32
36
|
}
|
|
33
37
|
},
|
|
34
|
-
"
|
|
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 };
|