@vuu-ui/vuu-data-editing 4.0.0-alpha.2 → 4.0.0-alpha.20

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.2",
2
+ "version": "4.0.0-alpha.20",
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.2",
16
- "@vuu-ui/vuu-utils": "4.0.0-alpha.2"
15
+ "@vuu-ui/vuu-ui-controls": "4.0.0-alpha.20",
16
+ "@vuu-ui/vuu-utils": "4.0.0-alpha.20"
17
17
  },
18
18
  "devDependencies": {
19
- "@vuu-ui/vuu-data-types": "4.0.0-alpha.2",
20
- "@vuu-ui/vuu-protocol-types": "4.0.0-alpha.2",
21
- "@vuu-ui/vuu-table-types": "4.0.0-alpha.2"
19
+ "@vuu-ui/vuu-data-types": "4.0.0-alpha.20",
20
+ "@vuu-ui/vuu-protocol-types": "4.0.0-alpha.20",
21
+ "@vuu-ui/vuu-table-types": "4.0.0-alpha.20"
22
22
  },
23
23
  "peerDependencies": {
24
24
  "react": "^19.2.3"
@@ -31,8 +31,9 @@
31
31
  ],
32
32
  "exports": {
33
33
  ".": {
34
+ "types": "./types/index.d.ts",
34
35
  "import": "./src/index.js",
35
- "types": "./types/index.d.ts"
36
+ "default": "./src/index.js"
36
37
  }
37
38
  },
38
39
  "module": "./src/index.js",
@@ -1,5 +1,6 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import { createContext, useContext } from "react";
3
+ import { EditSession } from "./EditSession.js";
3
4
  const DataEditingContext = /*#__PURE__*/ createContext(void 0);
4
5
  const DataEditingProvider = ({ children, editSession })=>/*#__PURE__*/ jsx(DataEditingContext.Provider, {
5
6
  value: editSession,
@@ -7,7 +8,8 @@ const DataEditingProvider = ({ children, editSession })=>/*#__PURE__*/ jsx(DataE
7
8
  });
8
9
  function useEditSession(throwIfUnavailable = false) {
9
10
  const editSession = useContext(DataEditingContext);
10
- if (void 0 !== editSession) return editSession;
11
- if (throwIfUnavailable) throw Error("[useEditSession] no DataEditingContext in scope. You need to enclose editable component(s) with DataEditingProvider");
11
+ if (editSession instanceof EditSession) return editSession;
12
+ if (throwIfUnavailable) throw Error(void 0 === editSession ? "[useEditSession] no DataEditingContext in scope. You need to enclose editable component(s) with DataEditingProvider" : "[useEditSession] DataEditingProvider does not provide an EditSession");
12
13
  }
13
- export { DataEditingProvider, useEditSession };
14
+ const useTableEditSession = ()=>useContext(DataEditingContext);
15
+ export { DataEditingProvider, useEditSession, useTableEditSession };
@@ -0,0 +1,35 @@
1
+ import { EventEmitter } from "@vuu-ui/vuu-utils";
2
+ class DirectEditSession extends EventEmitter {
3
+ #dataSource;
4
+ constructor({ dataSource }){
5
+ super();
6
+ this.#dataSource = dataSource;
7
+ }
8
+ get dataSource() {
9
+ return this.#dataSource;
10
+ }
11
+ get inEditMode() {
12
+ return false;
13
+ }
14
+ isCellEdited(_key, _columnName) {
15
+ return false;
16
+ }
17
+ async cancel(_key, _columnName, _restoredValue) {
18
+ return {
19
+ data: void 0,
20
+ type: "SUCCESS_RESULT"
21
+ };
22
+ }
23
+ async commit(key, columnName, _originalValue, typedValue, isValid) {
24
+ if (!isValid) return {
25
+ errorMessage: "Invalid value",
26
+ type: "ERROR_RESULT"
27
+ };
28
+ const editCell = this.#dataSource.editCell;
29
+ if (void 0 === editCell) throw Error("[DirectEditSession] datasource does not support editCell");
30
+ const response = await editCell.call(this.#dataSource, key, columnName, typedValue);
31
+ if (void 0 === response) throw Error("[DirectEditSession] datasource returned no response from editCell");
32
+ return response;
33
+ }
34
+ }
35
+ export { DirectEditSession };
@@ -1,8 +1,10 @@
1
1
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
2
  import { Button } from "@salt-ds/core";
3
- import { useCallback, useEffect, useState } from "react";
4
- const EditButtons = ({ canCancel, canSave, confirmCancel, confirmSave, editSession, hasSelection = false, onCancel, onDelete, onSave, saveLabel = "Save" })=>{
5
- const [editState, setEditState] = useState(()=>editSession?.editState ?? "clean");
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
@@ -1,11 +1,16 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
- import { createContext, useContext, useState } from "react";
2
+ import { createContext, useContext, useEffect, useState } from "react";
3
3
  const EditModeContext = /*#__PURE__*/ createContext({
4
4
  isEditMode: false,
5
5
  setEditMode: ()=>"EditModeProvider in place"
6
6
  });
7
- const EditModeProvider = ({ children })=>{
8
- const [isEditMode, setEditMode] = useState(false);
7
+ const EditModeProvider = ({ children, isEditMode: isEditModeProp = false })=>{
8
+ const [isEditMode, setEditMode] = useState(isEditModeProp);
9
+ useEffect(()=>{
10
+ setEditMode(isEditModeProp);
11
+ }, [
12
+ isEditModeProp
13
+ ]);
9
14
  return /*#__PURE__*/ jsx(EditModeContext.Provider, {
10
15
  value: {
11
16
  isEditMode,
@@ -66,6 +66,15 @@ class EditSession extends EventEmitter {
66
66
  if (this.#isStale) return "stale";
67
67
  return 0 === this.#editCount && 0 === this.#deleteCount && 0 === this.#addCount ? "clean" : "dirty";
68
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
+ }
69
78
  #emitEditStateChange(oldState, force = false) {
70
79
  const newState = this.editState;
71
80
  if (force || newState !== oldState) this.emit("editState", newState);
@@ -393,7 +402,8 @@ class EditSession extends EventEmitter {
393
402
  sessionDataSource
394
403
  });
395
404
  try {
396
- await this.dataSource?.endEditSession?.(saveChanges, force);
405
+ const response = await this.dataSource?.endEditSession?.(saveChanges, force);
406
+ if (isRpcError(response)) throw new Error(response.errorMessage);
397
407
  this.#clearEdits();
398
408
  this.#sessionDataSource = void 0;
399
409
  this.#setLifecycle({
@@ -468,15 +478,26 @@ class EditSession extends EventEmitter {
468
478
  #isLatestCellCommit(key, columnName, revision) {
469
479
  return this.#cellCommitRevisions.get(key)?.get(columnName) === revision;
470
480
  }
471
- async commit(key, columnName, originalValue, typedValue, isValid) {
481
+ async cancel(key, columnName, restoredValue) {
482
+ const cellEdits = this.#rowEdits.get(key)?.cellEdits;
483
+ const cellEdit = cellEdits?.get(columnName);
484
+ if (this.inEditMode && cellEdits && cellEdit && !cellEdit.isValid) this.#storeCellEdit(key, cellEdits, columnName, cellEdit.originalValue, restoredValue, true);
485
+ return {
486
+ data: void 0,
487
+ type: "SUCCESS_RESULT"
488
+ };
489
+ }
490
+ async commit(key, columnName, originalValue, typedValue, isValid, { dataSourceValue, syncToDataSource = true } = {}) {
472
491
  if ("active" !== this.#lifecycle.status && !("error" === this.#lifecycle.status && "end" === this.#lifecycle.operation)) throw new Error("No edit session in progress");
473
492
  const revision = this.#setCellCommitRevision(key, columnName);
474
493
  const rowEditDetails = this.#getOrCreateRowEdits(key);
475
494
  const { cellEdits } = rowEditDetails;
476
495
  if (isValid) {
477
496
  const cellEdit = this.#storeCellEdit(key, cellEdits, columnName, originalValue, typedValue, isValid);
478
- if (this.dataSource?.editCell) {
479
- const response = await this.dataSource.editCell(key, columnName, typedValue);
497
+ if (syncToDataSource && this.dataSource?.editCell) {
498
+ const valueForDataSource = dataSourceValue ?? typedValue;
499
+ if ("object" == typeof valueForDataSource && null !== valueForDataSource) throw new Error("Custom edit values require a scalar dataSourceValue.");
500
+ const response = await this.dataSource.editCell(key, columnName, valueForDataSource);
480
501
  if (!this.inEditMode) return {
481
502
  data: void 0,
482
503
  type: "SUCCESS_RESULT"
File without changes
@@ -0,0 +1,13 @@
1
+ const css = `
2
+ .vuuEditField {
3
+ & .saltDropdown {
4
+ --vuu-icon-height: 15px;
5
+ --vuu-icon-left: calc(100% - var(--salt-spacing-300));
6
+ --vuu-icon-size: 8px;
7
+ --vuu-icon-top: -3px;
8
+ }
9
+ }
10
+
11
+
12
+ `;
13
+ export default css;
@@ -0,0 +1,105 @@
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";
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 { inputProps, onChange, onDropdownSelectionChange, onKeyDown, value } = 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
+ bordered: true,
94
+ inputProps: {
95
+ ...inputProps,
96
+ onChange
97
+ },
98
+ onKeyDown: onKeyDown,
99
+ readOnly: readOnly,
100
+ value: value
101
+ })
102
+ ]
103
+ });
104
+ };
105
+ export { EditField };
@@ -0,0 +1,218 @@
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) await commit();
151
+ setEditState((editState)=>({
152
+ ...editState,
153
+ editing: false
154
+ }));
155
+ }
156
+ }, [
157
+ commit,
158
+ editState
159
+ ]);
160
+ const handleChange = useCallback((evt)=>{
161
+ const { value } = evt.target;
162
+ isDirtyRef.current = value !== initialValueRef.current;
163
+ const result = column.clientSideEditValidationCheck?.(value, "change");
164
+ setEditState({
165
+ editing: true,
166
+ value
167
+ });
168
+ onEdit?.({
169
+ editType: "change",
170
+ isValid: result?.ok !== false,
171
+ previousValue: initialValueRef.current,
172
+ value
173
+ }, "change");
174
+ if (result?.ok === false) setEditState({
175
+ editing: true,
176
+ value,
177
+ message: result.messages?.join(",")
178
+ });
179
+ }, [
180
+ column,
181
+ onEdit
182
+ ]);
183
+ const handleDropdownSelectionChange = useCallback((_evt, [selectedOption])=>{
184
+ if (!selectedOption) return;
185
+ const value = String(selectedOption.value);
186
+ isDirtyRef.current = value !== initialValueRef.current;
187
+ setEditState((state)=>({
188
+ ...state,
189
+ editing: true,
190
+ message: void 0,
191
+ value
192
+ }));
193
+ commit(value).then((committed)=>{
194
+ if (committed) setEditState((state)=>({
195
+ ...state,
196
+ editing: false,
197
+ value
198
+ }));
199
+ });
200
+ }, [
201
+ commit
202
+ ]);
203
+ return {
204
+ editing: editState.editing,
205
+ inputProps: {
206
+ onBlur: handleBlur,
207
+ onFocus: handleFocus,
208
+ type: textType
209
+ },
210
+ onKeyDown: handleKeyDown,
211
+ previousValue: editState.previousValue,
212
+ value: editState.value,
213
+ warningMessage: editState.message,
214
+ onChange: handleChange,
215
+ onDropdownSelectionChange: handleDropdownSelectionChange
216
+ };
217
+ };
218
+ export { useEditField };
package/src/edit-utils.js CHANGED
@@ -4,7 +4,7 @@ const timeFormatter = formatDate({
4
4
  time: "hh:mm:ss"
5
5
  });
6
6
  const dataRowEditErrors = Symbol("vuuDataRowEditErrors");
7
- const isEditRowReadOnly = (dataRow)=>"deleteRow" === dataRow.vuuAction;
7
+ const isEditRowReadOnly = (dataRow)=>dataRow.hasColumn("vuuAction") && "deleteRow" === dataRow.vuuAction;
8
8
  const withDataRowEditErrors = (dataRow, errors)=>{
9
9
  Object.defineProperty(dataRow, dataRowEditErrors, {
10
10
  configurable: true,
package/src/index.js CHANGED
@@ -1,10 +1,16 @@
1
- export { DataEditingProvider, useEditSession } from "./DataEditingProvider.js";
1
+ export { DataEditingProvider, useEditSession, useTableEditSession } from "./DataEditingProvider.js";
2
+ export { DirectEditSession } from "./DirectEditSession.js";
2
3
  export { useCellEdited } from "./useCellEdited.js";
4
+ export { useEditState } from "./useEditState.js";
3
5
  export { getVuuEditMessage, isEditRowReadOnly, isInlineEditingSession, withDataRowEditErrors } from "./edit-utils.js";
4
6
  export { EditButtons } from "./EditButtons.js";
7
+ export { EditField } from "./edit-field/EditField.js";
8
+ export { useEditField } from "./edit-field/useEditField.js";
5
9
  export { EditModeProvider, useEditMode } from "./EditModeProvider.js";
6
10
  export { EditError, EditSession, SupersededEditError } from "./EditSession.js";
7
11
  export { StaleUpdateError } from "@vuu-ui/vuu-utils";
12
+ export { lookupOptionsFromRows, useLookupValues } from "./lookup-values/useLookupValues.js";
13
+ export { useEditable } from "./useEditable.js";
8
14
  export { EDIT_ACTION_ROW_CLASS_NAME_GENERATOR, editActionRowClassNameGenerator } from "./editActionRowClassNameGenerator.js";
9
15
  export { UNDO_CELL_RENDERER, UndoCellRenderer, getUndoButtonContent, getUndoTooltipContent } from "./UndoCellRenderer.js";
10
16
  export { useEditableTable } from "./useEditableTable.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,53 @@
1
+ import { EditSession } from "./EditSession.js";
2
+ import { useEditMode } from "./EditModeProvider.js";
3
+ import { useCallback, useEffect, useMemo } from "react";
4
+ const useEditable = ({ copyOption = "Selected", dataSource, onCancel, onSave })=>{
5
+ const { isEditMode } = useEditMode();
6
+ const editSession = useMemo(()=>new EditSession({
7
+ dataSource
8
+ }), [
9
+ dataSource
10
+ ]);
11
+ const handleCancel = useCallback(async ()=>{
12
+ try {
13
+ await editSession.end();
14
+ onCancel();
15
+ } catch (error) {
16
+ console.error("[useEditable] cancel edit session failed", error);
17
+ }
18
+ }, [
19
+ editSession,
20
+ onCancel
21
+ ]);
22
+ const handleSave = useCallback(async (force = false)=>{
23
+ try {
24
+ await editSession.end(true, force);
25
+ onSave();
26
+ } catch (error) {
27
+ console.error("[useEditable] save edit session failed", error);
28
+ }
29
+ }, [
30
+ editSession,
31
+ onSave
32
+ ]);
33
+ useEffect(()=>{
34
+ const transition = isEditMode ? editSession.begin(copyOption) : editSession.end();
35
+ transition.catch((error)=>{
36
+ if (isEditMode) {
37
+ console.error("[useEditable] begin edit session failed", error);
38
+ handleCancel();
39
+ } else console.error("[useEditable] end edit session failed", error);
40
+ });
41
+ }, [
42
+ copyOption,
43
+ editSession,
44
+ isEditMode,
45
+ handleCancel
46
+ ]);
47
+ return {
48
+ editSession,
49
+ onCancel: handleCancel,
50
+ onSave: handleSave
51
+ };
52
+ };
53
+ export { useEditable };
@@ -1,8 +1,20 @@
1
1
  import { type ReactNode } from "react";
2
- import type { EditSession } from "./EditSession";
2
+ import { EditSession } from "./EditSession";
3
+ import type { TableEditSession } from "./TableEditSession";
3
4
  export declare const DataEditingProvider: ({ children, editSession, }: {
4
5
  children: ReactNode;
5
- editSession: EditSession;
6
+ editSession: TableEditSession;
6
7
  }) => import("react").JSX.Element;
8
+ /**
9
+ * Returns the EditSession provided by an enclosing DataEditingProvider. If the
10
+ * provider holds some other TableEditSession (e.g. DirectEditSession), there is
11
+ * no EditSession in scope and undefined is returned.
12
+ */
7
13
  export declare function useEditSession(throwIfUnavailable?: false): EditSession | undefined;
8
14
  export declare function useEditSession(throwIfUnavailable: true): EditSession;
15
+ /**
16
+ * Returns whichever TableEditSession is provided by an enclosing
17
+ * DataEditingProvider. Intended for use by Table cells, which commit and
18
+ * cancel edits regardless of the type of session.
19
+ */
20
+ export declare const useTableEditSession: () => TableEditSession | undefined;
@@ -0,0 +1,22 @@
1
+ import type { DataSource } from "@vuu-ui/vuu-data-types";
2
+ import type { RpcResult, VuuRowDataItemType } from "@vuu-ui/vuu-protocol-types";
3
+ import { EventEmitter } from "@vuu-ui/vuu-utils";
4
+ import type { TableEditSession, TableEditSessionEvents } from "./TableEditSession";
5
+ export type DirectEditSessionConstructorProps = {
6
+ dataSource: DataSource;
7
+ };
8
+ /**
9
+ * A lightweight edit session for services that allow cells to be edited
10
+ * directly on the source table. There is no begin/end lifecycle, no session
11
+ * table and no local tracking of edits - each committed edit is sent
12
+ * immediately via the dataSource editCell RPC.
13
+ */
14
+ export declare class DirectEditSession extends EventEmitter<TableEditSessionEvents> implements TableEditSession {
15
+ #private;
16
+ constructor({ dataSource }: DirectEditSessionConstructorProps);
17
+ get dataSource(): DataSource;
18
+ get inEditMode(): boolean;
19
+ isCellEdited(_key: string, _columnName: string): boolean;
20
+ cancel(_key: string, _columnName: string, _restoredValue: VuuRowDataItemType): Promise<RpcResult>;
21
+ commit(key: string, columnName: string, _originalValue: VuuRowDataItemType, typedValue: string | number | boolean, isValid: boolean): Promise<RpcResult>;
22
+ }
@@ -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;
@@ -1,8 +1,14 @@
1
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
+ import type { TableEditSession } from "./TableEditSession";
4
5
  export type EditState = "clean" | "dirty" | "invalid" | "stale";
5
6
  export type EditActionType = "deleteRow" | "addRow" | "editCell";
7
+ export type EditSessionValue = VuuRowDataItemType | object;
8
+ export type EditCommitOptions = {
9
+ dataSourceValue?: VuuRowDataItemType;
10
+ syncToDataSource?: boolean;
11
+ };
6
12
  /** Column name to default value mapping applied to every addRow call when a column is absent from the row data. */
7
13
  export type RowDefaultDataItemValues = Record<string, VuuRowDataItemType>;
8
14
  export type EditSessionApi = "createSessionDataSource" | "beginEditSession";
@@ -49,7 +55,7 @@ type EditSessionEvents = {
49
55
  lifecycle: (lifecycle: EditLifecycle) => void;
50
56
  newRow: (newRowState: NewRowState) => void;
51
57
  };
52
- export declare class EditSession extends EventEmitter<EditSessionEvents> {
58
+ export declare class EditSession extends EventEmitter<EditSessionEvents> implements TableEditSession {
53
59
  #private;
54
60
  static readonly newRowKey = "__vuu_new_row__";
55
61
  constructor({ dataSource, deleteMode, editSessionApi, rowDefaults, }: EditSessionConstructorProps);
@@ -58,6 +64,9 @@ export declare class EditSession extends EventEmitter<EditSessionEvents> {
58
64
  get deleteCount(): number;
59
65
  get addCount(): number;
60
66
  get editState(): EditState;
67
+ get isDirty(): boolean;
68
+ get canCancel(): boolean;
69
+ get canSave(): boolean;
61
70
  get newRowState(): NewRowState;
62
71
  isNewRow(key: string): key is "__vuu_new_row__";
63
72
  isNewRowFinalColumn(columnName: string): boolean;
@@ -81,6 +90,7 @@ export declare class EditSession extends EventEmitter<EditSessionEvents> {
81
90
  end(saveChanges?: boolean, force?: boolean): Promise<void>;
82
91
  get lifecycle(): EditLifecycle;
83
92
  get inEditMode(): boolean;
84
- commit(key: string, columnName: string, originalValue: VuuRowDataItemType, typedValue: string | number | boolean, isValid: boolean): Promise<RpcResult>;
93
+ cancel(key: string, columnName: string, restoredValue: VuuRowDataItemType): Promise<RpcResult>;
94
+ commit(key: string, columnName: string, originalValue: EditSessionValue, typedValue: EditSessionValue, isValid: boolean, { dataSourceValue, syncToDataSource }?: EditCommitOptions): Promise<RpcResult>;
85
95
  }
86
96
  export {};
@@ -0,0 +1,28 @@
1
+ import type { RpcResult, VuuRowDataItemType } from "@vuu-ui/vuu-protocol-types";
2
+ export type TableEditSessionEvents = {
3
+ cellEditChanged: (key: string, columnName: string) => void;
4
+ };
5
+ /**
6
+ * The subset of edit session behaviour consumed by Table and its cells.
7
+ * Implemented by EditSession (edits staged in a session table, committed or
8
+ * discarded at end of session) and DirectEditSession (edits applied directly
9
+ * to the source table).
10
+ */
11
+ export interface TableEditSession {
12
+ /**
13
+ * True while a staged edit session is in progress. Always false for
14
+ * DirectEditSession.
15
+ */
16
+ readonly inEditMode: boolean;
17
+ commit(key: string, columnName: string, originalValue: VuuRowDataItemType, typedValue: string | number | boolean, isValid: boolean): Promise<RpcResult>;
18
+ /**
19
+ * Invoked when user abandons an in-progress cell edit, restoring the value
20
+ * the cell held before editing began. Values that have only been typed, not
21
+ * committed, have never been sent to the server, so nothing is sent here.
22
+ * Any invalid edit previously recorded for the cell is discarded.
23
+ */
24
+ cancel(key: string, columnName: string, restoredValue: VuuRowDataItemType): Promise<RpcResult>;
25
+ isCellEdited(key: string, columnName: string): boolean;
26
+ on<E extends keyof TableEditSessionEvents>(event: E, listener: TableEditSessionEvents[E]): void;
27
+ removeListener<E extends keyof TableEditSessionEvents>(event: E, listener: TableEditSessionEvents[E]): void;
28
+ }
@@ -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<HTMLInputElement>;
25
+ onDropdownSelectionChange: (_evt: SyntheticEvent, [selectedOption]: LookupOption[]) => void;
26
+ };
package/types/index.d.ts CHANGED
@@ -1,10 +1,17 @@
1
- export { DataEditingProvider, useEditSession } from "./DataEditingProvider";
1
+ export { DataEditingProvider, useEditSession, useTableEditSession, } from "./DataEditingProvider";
2
+ export { DirectEditSession, type DirectEditSessionConstructorProps, } from "./DirectEditSession";
3
+ export type { TableEditSession, TableEditSessionEvents, } from "./TableEditSession";
2
4
  export { useCellEdited } from "./useCellEdited";
5
+ export { useEditState } from "./useEditState";
3
6
  export { getVuuEditMessage, isInlineEditingSession, isEditRowReadOnly, withDataRowEditErrors, } from "./edit-utils";
4
7
  export { EditButtons, type EditButtonProps } from "./EditButtons";
8
+ export { EditField, type DropdownEditFieldProps, type EditFieldProps, type EditFieldType, type TextFieldType, } from "./edit-field/EditField";
9
+ export { useEditField, type EditFieldHookProps, } from "./edit-field/useEditField";
5
10
  export { EditModeProvider, useEditMode, type EditModeContextProps, } from "./EditModeProvider";
6
- export { EditError, EditSession, SupersededEditError, type EditActionType, type EditLifecycle, type EditSessionConstructorProps, type NewRowState, type EditState, type RowDefaultDataItemValues, } from "./EditSession";
11
+ export { EditError, EditSession, SupersededEditError, type EditActionType, type EditCommitOptions, type EditLifecycle, type EditSessionConstructorProps, type EditSessionValue, type NewRowState, type EditState, type RowDefaultDataItemValues, } from "./EditSession";
7
12
  export { StaleUpdateError } from "@vuu-ui/vuu-utils";
13
+ export { lookupOptionsFromRows, useLookupValues, type LookupOption, type LookupValuedHookProps, type OptionMap, } from "./lookup-values/useLookupValues";
14
+ export { useEditable, type EditableHookProps } from "./useEditable";
8
15
  export { EDIT_ACTION_ROW_CLASS_NAME_GENERATOR, editActionRowClassNameGenerator, } from "./editActionRowClassNameGenerator";
9
16
  export { getUndoButtonContent, getUndoTooltipContent, UNDO_CELL_RENDERER, UndoCellRenderer, type UndoCellRendererComponentProps, } from "./UndoCellRenderer";
10
17
  export { useEditableTable, type EditableTableHookProps, type EditMode, } from "./useEditableTable";
@@ -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[];
@@ -1,2 +1,2 @@
1
- import type { EditSession } from "./EditSession";
2
- export declare const useCellEdited: (editSession: EditSession | undefined, key: string, columnName: string) => boolean;
1
+ import type { TableEditSession } from "./TableEditSession";
2
+ export declare const useCellEdited: (editSession: TableEditSession | undefined, key: string, columnName: string) => boolean;
@@ -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 "./EditSession";
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
+ };