@vuu-ui/vuu-data-editing 4.0.0-alpha.2 → 4.0.0-alpha.21
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/README.md +364 -1
- package/package.json +9 -8
- package/src/DataEditingProvider.js +5 -3
- package/src/DirectEditSession.js +35 -0
- package/src/EditButtons.js +8 -14
- package/src/EditModeProvider.js +8 -3
- package/src/EditSession.js +55 -14
- package/src/TableEditSession.js +0 -0
- package/src/edit-field/EditField.css.js +13 -0
- package/src/edit-field/EditField.js +105 -0
- package/src/edit-field/useEditField.js +218 -0
- package/src/edit-utils.js +1 -1
- package/src/index.js +7 -1
- package/src/lookup-values/useLookupValues.js +78 -0
- package/src/useEditState.js +15 -0
- package/src/useEditable.js +53 -0
- package/src/useEditableTable.js +2 -0
- package/types/DataEditingProvider.d.ts +15 -3
- package/types/DirectEditSession.d.ts +22 -0
- package/types/EditButtons.d.ts +1 -1
- package/types/EditModeProvider.d.ts +4 -3
- package/types/EditSession.d.ts +15 -2
- package/types/TableEditSession.d.ts +28 -0
- package/types/UndoCellRenderer.d.ts +1 -1
- package/types/edit-field/EditField.d.ts +21 -0
- package/types/edit-field/useEditField.d.ts +26 -0
- package/types/edit-utils.d.ts +1 -1
- package/types/index.d.ts +9 -2
- package/types/lookup-values/useLookupValues.d.ts +19 -0
- package/types/useCellEdited.d.ts +2 -2
- package/types/useEditState.d.ts +2 -0
- package/types/useEditable.d.ts +13 -0
- package/types/useEditableTable.d.ts +2 -0
|
File without changes
|
|
@@ -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 };
|
package/src/useEditableTable.js
CHANGED
|
@@ -2,6 +2,7 @@ import { useData, useLayoutEffectSkipFirst } from "@vuu-ui/vuu-utils";
|
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { EditSession } from "./EditSession.js";
|
|
4
4
|
import { EDIT_ACTION_ROW_CLASS_NAME_GENERATOR } from "./editActionRowClassNameGenerator.js";
|
|
5
|
+
import { isEditRowReadOnly } from "./edit-utils.js";
|
|
5
6
|
const EDIT_ACTION_ROW_CLASS_NAME_GENERATORS = [
|
|
6
7
|
EDIT_ACTION_ROW_CLASS_NAME_GENERATOR
|
|
7
8
|
];
|
|
@@ -148,6 +149,7 @@ const useEditableTable = ({ columns, dataSource: dataSourceProp, deleteMode = "s
|
|
|
148
149
|
onDelete: handleDelete,
|
|
149
150
|
onSave: handleSave,
|
|
150
151
|
onUndoRowChange: handleUndoRowChange,
|
|
152
|
+
isRowSelectable: isEditMode ? (dataRow)=>!isEditRowReadOnly(dataRow) : void 0,
|
|
151
153
|
rowClassNameGenerators: isEditMode ? EDIT_ACTION_ROW_CLASS_NAME_GENERATORS : void 0,
|
|
152
154
|
sessionDataSource,
|
|
153
155
|
sourceDataSource
|
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
|
-
import
|
|
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:
|
|
6
|
-
}) => import("react").JSX.Element;
|
|
6
|
+
editSession: TableEditSession;
|
|
7
|
+
}) => import("react/jsx-runtime").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
|
+
}
|
package/types/EditButtons.d.ts
CHANGED
|
@@ -11,4 +11,4 @@ export interface EditButtonProps {
|
|
|
11
11
|
confirmSave?: () => boolean | Promise<boolean>;
|
|
12
12
|
confirmCancel?: () => boolean | Promise<boolean>;
|
|
13
13
|
}
|
|
14
|
-
export declare const EditButtons: ({ canCancel, canSave, confirmCancel, confirmSave, editSession, hasSelection, onCancel, onDelete, onSave, saveLabel, }: EditButtonProps) => import("react").JSX.Element;
|
|
14
|
+
export declare const EditButtons: ({ canCancel, canSave: _canSave, confirmCancel, confirmSave, editSession, hasSelection, onCancel, onDelete, onSave, saveLabel, }: EditButtonProps) => import("react/jsx-runtime").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
|
-
|
|
12
|
+
isEditMode?: boolean;
|
|
13
|
+
}) => import("react/jsx-runtime").JSX.Element;
|
|
13
14
|
export declare const useEditMode: () => EditModeContextProps;
|
package/types/EditSession.d.ts
CHANGED
|
@@ -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";
|
|
@@ -15,6 +21,9 @@ export type EditSessionConstructorProps = {
|
|
|
15
21
|
/** Default column values merged into every addRow call for absent columns. Pass a stable reference. */
|
|
16
22
|
rowDefaults?: RowDefaultDataItemValues;
|
|
17
23
|
};
|
|
24
|
+
export type AddRowResultData = {
|
|
25
|
+
key?: string | number;
|
|
26
|
+
};
|
|
18
27
|
export type NewRowState = {
|
|
19
28
|
columns: readonly string[];
|
|
20
29
|
requiredColumns?: readonly string[];
|
|
@@ -49,7 +58,7 @@ type EditSessionEvents = {
|
|
|
49
58
|
lifecycle: (lifecycle: EditLifecycle) => void;
|
|
50
59
|
newRow: (newRowState: NewRowState) => void;
|
|
51
60
|
};
|
|
52
|
-
export declare class EditSession extends EventEmitter<EditSessionEvents> {
|
|
61
|
+
export declare class EditSession extends EventEmitter<EditSessionEvents> implements TableEditSession {
|
|
53
62
|
#private;
|
|
54
63
|
static readonly newRowKey = "__vuu_new_row__";
|
|
55
64
|
constructor({ dataSource, deleteMode, editSessionApi, rowDefaults, }: EditSessionConstructorProps);
|
|
@@ -58,6 +67,9 @@ export declare class EditSession extends EventEmitter<EditSessionEvents> {
|
|
|
58
67
|
get deleteCount(): number;
|
|
59
68
|
get addCount(): number;
|
|
60
69
|
get editState(): EditState;
|
|
70
|
+
get isDirty(): boolean;
|
|
71
|
+
get canCancel(): boolean;
|
|
72
|
+
get canSave(): boolean;
|
|
61
73
|
get newRowState(): NewRowState;
|
|
62
74
|
isNewRow(key: string): key is "__vuu_new_row__";
|
|
63
75
|
isNewRowFinalColumn(columnName: string): boolean;
|
|
@@ -81,6 +93,7 @@ export declare class EditSession extends EventEmitter<EditSessionEvents> {
|
|
|
81
93
|
end(saveChanges?: boolean, force?: boolean): Promise<void>;
|
|
82
94
|
get lifecycle(): EditLifecycle;
|
|
83
95
|
get inEditMode(): boolean;
|
|
84
|
-
|
|
96
|
+
cancel(key: string, columnName: string, restoredValue: VuuRowDataItemType): Promise<RpcResult>;
|
|
97
|
+
commit(key: string, columnName: string, originalValue: EditSessionValue, typedValue: EditSessionValue, isValid: boolean, { dataSourceValue, syncToDataSource }?: EditCommitOptions): Promise<RpcResult>;
|
|
85
98
|
}
|
|
86
99
|
export {};
|