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

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