@vuu-ui/vuu-data-editing 5.0.0-alpha.2 → 5.0.0-alpha.4
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 +40 -3
- package/package.json +7 -6
- package/src/DirectEditSession.js +8 -1
- package/src/EditButtons.js +6 -5
- package/src/EditModeProvider.js +14 -5
- package/src/EditSession.js +78 -8
- package/src/edit-control/EditControlCheckbox.js +25 -0
- package/src/edit-control/EditControlDropdown.js +34 -0
- package/src/edit-control/EditControlLookupDropdown.js +31 -0
- package/src/edit-control/get-data-item-edit-control.js +139 -0
- package/src/edit-control/index.js +4 -0
- package/src/edit-errors.js +7 -0
- package/src/edit-field/EditField.js +117 -24
- package/src/edit-field/useEditField.js +66 -52
- package/src/edit-form/CreateRowForm.js +37 -0
- package/src/edit-form/EditForm.css.js +32 -0
- package/src/edit-form/EditForm.js +85 -0
- package/src/edit-form/EditFormButtons.js +48 -0
- package/src/edit-form/EditFormContext.js +4 -0
- package/src/edit-form/index.js +4 -0
- package/src/form-fields.js +40 -0
- package/src/index.js +9 -0
- package/src/lookup-values/useLookupOptions.js +106 -0
- package/src/useConfirmDiscard.js +34 -0
- package/src/useCustomEditField.js +76 -0
- package/src/useEditForm.js +306 -0
- package/src/useEditSessionState.js +72 -0
- package/src/useEditable.js +26 -9
- package/src/useEditableColumns.js +105 -0
- package/src/useEditableTable.js +32 -9
- package/types/DirectEditSession.d.ts +6 -1
- package/types/EditButtons.d.ts +20 -3
- package/types/EditModeProvider.d.ts +10 -2
- package/types/EditSession.d.ts +18 -1
- package/types/edit-control/EditControlCheckbox.d.ts +8 -0
- package/types/edit-control/EditControlDropdown.d.ts +14 -0
- package/types/edit-control/EditControlLookupDropdown.d.ts +13 -0
- package/types/edit-control/get-data-item-edit-control.d.ts +40 -0
- package/types/edit-control/index.d.ts +4 -0
- package/types/edit-errors.d.ts +9 -0
- package/types/edit-field/EditField.d.ts +37 -3
- package/types/edit-field/useEditField.d.ts +2 -1
- package/types/edit-form/CreateRowForm.d.ts +29 -0
- package/types/edit-form/EditForm.d.ts +36 -0
- package/types/edit-form/EditFormButtons.d.ts +18 -0
- package/types/edit-form/EditFormContext.d.ts +59 -0
- package/types/edit-form/index.d.ts +4 -0
- package/types/form-fields.d.ts +32 -0
- package/types/index.d.ts +12 -2
- package/types/lookup-values/useLookupOptions.d.ts +35 -0
- package/types/useConfirmDiscard.d.ts +23 -0
- package/types/useCustomEditField.d.ts +51 -0
- package/types/useEditForm.d.ts +127 -0
- package/types/useEditSessionState.d.ts +27 -0
- package/types/useEditable.d.ts +30 -3
- package/types/useEditableColumns.d.ts +79 -0
- package/types/useEditableTable.d.ts +18 -3
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Range, useData } from "@vuu-ui/vuu-utils";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { lookupOptionsFromRows } from "./useLookupValues.js";
|
|
4
|
+
const DATA_ROW_COLUMN_OFFSET = 10;
|
|
5
|
+
const rowToRecord = (columns, row)=>Object.fromEntries(columns.map((column, i)=>[
|
|
6
|
+
column,
|
|
7
|
+
row[i + DATA_ROW_COLUMN_OFFSET]
|
|
8
|
+
]));
|
|
9
|
+
const useLookupOptions = ({ columns: columnsProp, enabled = true, filter, mapRow, maxRows = 100, optionMap, table })=>{
|
|
10
|
+
const { VuuDataSource } = useData();
|
|
11
|
+
const [state, setState] = useState({
|
|
12
|
+
loading: false,
|
|
13
|
+
options: []
|
|
14
|
+
});
|
|
15
|
+
const mapRowRef = useRef(mapRow);
|
|
16
|
+
mapRowRef.current = mapRow;
|
|
17
|
+
const columns = useMemo(()=>{
|
|
18
|
+
if (columnsProp) return [
|
|
19
|
+
...columnsProp
|
|
20
|
+
];
|
|
21
|
+
if (optionMap) return [
|
|
22
|
+
optionMap.value,
|
|
23
|
+
optionMap.label,
|
|
24
|
+
...optionMap.additionalFields ?? []
|
|
25
|
+
];
|
|
26
|
+
return [];
|
|
27
|
+
}, [
|
|
28
|
+
columnsProp,
|
|
29
|
+
optionMap
|
|
30
|
+
]);
|
|
31
|
+
const dataSource = useMemo(()=>enabled && table && columns.length > 0 ? new VuuDataSource({
|
|
32
|
+
columns,
|
|
33
|
+
filterSpec: filter ? {
|
|
34
|
+
filter
|
|
35
|
+
} : void 0,
|
|
36
|
+
table
|
|
37
|
+
}) : void 0, [
|
|
38
|
+
VuuDataSource,
|
|
39
|
+
columns,
|
|
40
|
+
enabled,
|
|
41
|
+
filter,
|
|
42
|
+
table
|
|
43
|
+
]);
|
|
44
|
+
useEffect(()=>{
|
|
45
|
+
if (!dataSource) return void setState({
|
|
46
|
+
loading: false,
|
|
47
|
+
options: []
|
|
48
|
+
});
|
|
49
|
+
if (!mapRowRef.current && !optionMap) return void setState({
|
|
50
|
+
error: new Error("[useLookupOptions] either optionMap or mapRow must be provided"),
|
|
51
|
+
loading: false,
|
|
52
|
+
options: []
|
|
53
|
+
});
|
|
54
|
+
setState({
|
|
55
|
+
loading: true,
|
|
56
|
+
options: []
|
|
57
|
+
});
|
|
58
|
+
let active = true;
|
|
59
|
+
let subscribedColumns;
|
|
60
|
+
const fail = (error)=>{
|
|
61
|
+
if (active) setState({
|
|
62
|
+
error,
|
|
63
|
+
loading: false,
|
|
64
|
+
options: []
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
const subscribe = (message)=>{
|
|
68
|
+
if (!active) return;
|
|
69
|
+
if ("subscribed" === message.type) subscribedColumns = message.columns;
|
|
70
|
+
else if ("viewport-update" === message.type && subscribedColumns) {
|
|
71
|
+
if (void 0 === message.rows) {
|
|
72
|
+
if (0 === message.size) setState({
|
|
73
|
+
loading: false,
|
|
74
|
+
options: []
|
|
75
|
+
});
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const rows = message.rows.slice(0, maxRows);
|
|
80
|
+
const mapper = mapRowRef.current;
|
|
81
|
+
const options = mapper ? rows.map((row)=>mapper(rowToRecord(subscribedColumns, row))) : lookupOptionsFromRows(subscribedColumns, rows, optionMap);
|
|
82
|
+
setState({
|
|
83
|
+
loading: false,
|
|
84
|
+
options
|
|
85
|
+
});
|
|
86
|
+
} catch (cause) {
|
|
87
|
+
fail(cause instanceof Error ? cause : new Error(String(cause)));
|
|
88
|
+
}
|
|
89
|
+
} else if ("subscribe-failed" === message.type) fail(new Error(`[useLookupOptions] subscription to ${table?.table} failed: ${message.msg}`));
|
|
90
|
+
};
|
|
91
|
+
dataSource.subscribe({
|
|
92
|
+
range: Range(0, maxRows)
|
|
93
|
+
}, subscribe).catch((cause)=>fail(cause instanceof Error ? cause : new Error(String(cause))));
|
|
94
|
+
return ()=>{
|
|
95
|
+
active = false;
|
|
96
|
+
dataSource.unsubscribe();
|
|
97
|
+
};
|
|
98
|
+
}, [
|
|
99
|
+
dataSource,
|
|
100
|
+
maxRows,
|
|
101
|
+
optionMap,
|
|
102
|
+
table
|
|
103
|
+
]);
|
|
104
|
+
return state;
|
|
105
|
+
};
|
|
106
|
+
export { useLookupOptions };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useModal } from "@vuu-ui/core";
|
|
2
|
+
import { createElement, useCallback } from "react";
|
|
3
|
+
const useConfirmDiscard = ({ cancelButtonLabel = "Keep editing", confirmButtonLabel = "Discard", message = "You have unsaved changes. Discard them?", title = "Discard changes?" } = {})=>{
|
|
4
|
+
const { closePrompt, showPrompt } = useModal();
|
|
5
|
+
return useCallback(()=>new Promise((resolve)=>{
|
|
6
|
+
let resolved = false;
|
|
7
|
+
const settle = (confirmed)=>{
|
|
8
|
+
if (!resolved) {
|
|
9
|
+
resolved = true;
|
|
10
|
+
resolve(confirmed);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
showPrompt(createElement("span", null, message), {
|
|
14
|
+
cancelButtonLabel,
|
|
15
|
+
confirmButtonLabel,
|
|
16
|
+
initialFocusedItem: "cancel",
|
|
17
|
+
onCancel: ()=>settle(false),
|
|
18
|
+
onConfirm: ()=>settle(true),
|
|
19
|
+
onOpenChange: (open)=>{
|
|
20
|
+
if (!open) settle(false);
|
|
21
|
+
},
|
|
22
|
+
status: "warning",
|
|
23
|
+
title
|
|
24
|
+
});
|
|
25
|
+
}).finally(closePrompt), [
|
|
26
|
+
cancelButtonLabel,
|
|
27
|
+
closePrompt,
|
|
28
|
+
confirmButtonLabel,
|
|
29
|
+
message,
|
|
30
|
+
showPrompt,
|
|
31
|
+
title
|
|
32
|
+
]);
|
|
33
|
+
};
|
|
34
|
+
export { useConfirmDiscard };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { isRpcError } from "@vuu-ui/vuu-utils";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
import { useEditSession } from "./DataEditingProvider.js";
|
|
4
|
+
import { toError } from "./edit-errors.js";
|
|
5
|
+
import { useEditSessionState } from "./useEditSessionState.js";
|
|
6
|
+
const useCustomEditField = ({ editSession: editSessionProp, equals = Object.is, name, onError, originalValue, rowKey, serialize })=>{
|
|
7
|
+
const contextEditSession = useEditSession();
|
|
8
|
+
const editSession = editSessionProp ?? contextEditSession;
|
|
9
|
+
const { inEditMode } = useEditSessionState(editSession);
|
|
10
|
+
const [value, setValueState] = useState(originalValue);
|
|
11
|
+
const [error, setError] = useState();
|
|
12
|
+
const [committing, setCommitting] = useState(false);
|
|
13
|
+
const onErrorRef = useRef(onError);
|
|
14
|
+
onErrorRef.current = onError;
|
|
15
|
+
const latestCommitRef = useRef(0);
|
|
16
|
+
useEffect(()=>{
|
|
17
|
+
if (!inEditMode) {
|
|
18
|
+
setValueState(originalValue);
|
|
19
|
+
setError(void 0);
|
|
20
|
+
}
|
|
21
|
+
}, [
|
|
22
|
+
inEditMode,
|
|
23
|
+
originalValue
|
|
24
|
+
]);
|
|
25
|
+
const setValue = useCallback(async (nextValue)=>{
|
|
26
|
+
if (!editSession) throw Error("[useCustomEditField] no EditSession, pass editSession or use a DataEditingProvider");
|
|
27
|
+
const original = originalValue ?? nextValue;
|
|
28
|
+
const committedValue = void 0 !== originalValue && equals(nextValue, originalValue) ? originalValue : nextValue;
|
|
29
|
+
const commitId = ++latestCommitRef.current;
|
|
30
|
+
setValueState(committedValue);
|
|
31
|
+
setCommitting(true);
|
|
32
|
+
setError(void 0);
|
|
33
|
+
try {
|
|
34
|
+
const response = await editSession.commit(rowKey, name, original, committedValue, true, serialize ? {
|
|
35
|
+
dataSourceValue: serialize(committedValue)
|
|
36
|
+
} : {});
|
|
37
|
+
if (isRpcError(response) && commitId === latestCommitRef.current) {
|
|
38
|
+
const err = new Error(response.errorMessage);
|
|
39
|
+
setError(err);
|
|
40
|
+
onErrorRef.current?.(err);
|
|
41
|
+
}
|
|
42
|
+
} catch (cause) {
|
|
43
|
+
if (commitId === latestCommitRef.current) {
|
|
44
|
+
const err = toError(cause);
|
|
45
|
+
setError(err);
|
|
46
|
+
if (onErrorRef.current) onErrorRef.current(err);
|
|
47
|
+
else console.error("[useCustomEditField] commit %s failed", name, err);
|
|
48
|
+
}
|
|
49
|
+
} finally{
|
|
50
|
+
if (commitId === latestCommitRef.current) setCommitting(false);
|
|
51
|
+
}
|
|
52
|
+
}, [
|
|
53
|
+
editSession,
|
|
54
|
+
equals,
|
|
55
|
+
name,
|
|
56
|
+
originalValue,
|
|
57
|
+
rowKey,
|
|
58
|
+
serialize
|
|
59
|
+
]);
|
|
60
|
+
const reset = useCallback(()=>{
|
|
61
|
+
setValueState(originalValue);
|
|
62
|
+
setError(void 0);
|
|
63
|
+
}, [
|
|
64
|
+
originalValue
|
|
65
|
+
]);
|
|
66
|
+
const isDirty = void 0 !== value && void 0 !== originalValue && !equals(value, originalValue);
|
|
67
|
+
return {
|
|
68
|
+
committing,
|
|
69
|
+
error,
|
|
70
|
+
isDirty,
|
|
71
|
+
reset,
|
|
72
|
+
setValue,
|
|
73
|
+
value
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
export { useCustomEditField };
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { isRpcError, isRpcSuccess } from "@vuu-ui/vuu-utils";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { EditSession } from "./EditSession.js";
|
|
4
|
+
import { DirectEditSession } from "./DirectEditSession.js";
|
|
5
|
+
import { useEditMode } from "./EditModeProvider.js";
|
|
6
|
+
import { reportEditError, toError } from "./edit-errors.js";
|
|
7
|
+
import { useEditSessionState } from "./useEditSessionState.js";
|
|
8
|
+
const EMPTY_ERRORS = {};
|
|
9
|
+
const NEW_ROW = {
|
|
10
|
+
key: EditSession.newRowKey
|
|
11
|
+
};
|
|
12
|
+
const getDataRowValues = (dataRow)=>{
|
|
13
|
+
const { toJSON } = dataRow;
|
|
14
|
+
const values = "function" == typeof toJSON ? toJSON.call(dataRow) : {
|
|
15
|
+
...dataRow
|
|
16
|
+
};
|
|
17
|
+
return {
|
|
18
|
+
...values,
|
|
19
|
+
key: dataRow.key
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
const isEmptyValue = (value)=>null == value || "string" == typeof value && "" === value.trim();
|
|
23
|
+
const toRowSnapshot = (key, values)=>({
|
|
24
|
+
...values,
|
|
25
|
+
key
|
|
26
|
+
});
|
|
27
|
+
class FormDirectEditSession extends DirectEditSession {
|
|
28
|
+
#onCommitted;
|
|
29
|
+
constructor(props, onCommitted){
|
|
30
|
+
super(props);
|
|
31
|
+
this.#onCommitted = onCommitted;
|
|
32
|
+
}
|
|
33
|
+
async commit(key, columnName, originalValue, typedValue, isValid) {
|
|
34
|
+
const response = await super.commit(key, columnName, originalValue, typedValue, isValid);
|
|
35
|
+
if (isRpcSuccess(response)) this.#onCommitted(columnName, typedValue);
|
|
36
|
+
return response;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const hasErrors = (errors)=>null != errors && Object.keys(errors).length > 0;
|
|
40
|
+
const useEditForm = ({ columns, dataRow, dataSource, deleteMode, editModel = "session", editSession: sharedEditSession, editSessionApi, isEditMode: isEditModeProp, mode = "edit", onCancelled, onError, onSaved, requiredColumns, rowDefaults, validate })=>{
|
|
41
|
+
const { isEditMode: isEditModeContext } = useEditMode();
|
|
42
|
+
const isCreate = "create" === mode;
|
|
43
|
+
const isDirect = !isCreate && "direct" === editModel;
|
|
44
|
+
const isShared = isCreate && void 0 !== sharedEditSession;
|
|
45
|
+
const ownsSession = !isDirect && !isShared;
|
|
46
|
+
const isEditMode = isCreate || (isEditModeProp ?? isEditModeContext);
|
|
47
|
+
const [saving, setSaving] = useState(false);
|
|
48
|
+
const [error, setError] = useState();
|
|
49
|
+
const [validationErrors, setValidationErrors] = useState(EMPTY_ERRORS);
|
|
50
|
+
const savingRef = useRef(false);
|
|
51
|
+
const rowAddedRef = useRef(false);
|
|
52
|
+
const [rowAdded, setRowAdded] = useState(false);
|
|
53
|
+
const markRowAdded = useCallback((added)=>{
|
|
54
|
+
rowAddedRef.current = added;
|
|
55
|
+
setRowAdded(added);
|
|
56
|
+
}, []);
|
|
57
|
+
const callbacksRef = useRef({
|
|
58
|
+
onCancelled,
|
|
59
|
+
onError,
|
|
60
|
+
onSaved,
|
|
61
|
+
validate
|
|
62
|
+
});
|
|
63
|
+
callbacksRef.current = {
|
|
64
|
+
onCancelled,
|
|
65
|
+
onError,
|
|
66
|
+
onSaved,
|
|
67
|
+
validate
|
|
68
|
+
};
|
|
69
|
+
const editSession = useMemo(()=>sharedEditSession ?? new EditSession({
|
|
70
|
+
dataSource,
|
|
71
|
+
deleteMode,
|
|
72
|
+
editSessionApi,
|
|
73
|
+
rowDefaults
|
|
74
|
+
}), [
|
|
75
|
+
dataSource,
|
|
76
|
+
deleteMode,
|
|
77
|
+
editSessionApi,
|
|
78
|
+
rowDefaults,
|
|
79
|
+
sharedEditSession
|
|
80
|
+
]);
|
|
81
|
+
const [directValues, setDirectValues] = useState({});
|
|
82
|
+
const directRowKey = isDirect ? dataRow?.key : void 0;
|
|
83
|
+
useEffect(()=>{
|
|
84
|
+
setDirectValues({});
|
|
85
|
+
}, [
|
|
86
|
+
directRowKey
|
|
87
|
+
]);
|
|
88
|
+
const directValuesRef = useRef(directValues);
|
|
89
|
+
directValuesRef.current = directValues;
|
|
90
|
+
const dataRowRef = useRef(dataRow);
|
|
91
|
+
dataRowRef.current = dataRow;
|
|
92
|
+
const requiredRef = useRef(requiredColumns);
|
|
93
|
+
requiredRef.current = requiredColumns;
|
|
94
|
+
const directEditSession = useMemo(()=>isDirect ? new FormDirectEditSession({
|
|
95
|
+
dataSource,
|
|
96
|
+
validate: (_key, columnName, value)=>{
|
|
97
|
+
if (requiredRef.current?.includes(columnName) && isEmptyValue(value)) return "A value is required";
|
|
98
|
+
const { current: row } = dataRowRef;
|
|
99
|
+
const values = {
|
|
100
|
+
...row ? getDataRowValues(row) : void 0,
|
|
101
|
+
...directValuesRef.current,
|
|
102
|
+
[columnName]: value
|
|
103
|
+
};
|
|
104
|
+
return callbacksRef.current.validate?.(values)?.[columnName];
|
|
105
|
+
}
|
|
106
|
+
}, (columnName, value)=>setDirectValues((values)=>({
|
|
107
|
+
...values,
|
|
108
|
+
[columnName]: value
|
|
109
|
+
}))) : void 0, [
|
|
110
|
+
dataSource,
|
|
111
|
+
isDirect
|
|
112
|
+
]);
|
|
113
|
+
const sessionState = useEditSessionState(editSession);
|
|
114
|
+
const [, setValuesRevision] = useState(0);
|
|
115
|
+
const rowKey = dataRow?.key;
|
|
116
|
+
useEffect(()=>{
|
|
117
|
+
if (isCreate || void 0 === rowKey) return;
|
|
118
|
+
const onCellEditChanged = (key)=>{
|
|
119
|
+
if (key === rowKey) setValuesRevision((n)=>n + 1);
|
|
120
|
+
};
|
|
121
|
+
editSession.on("cellEditChanged", onCellEditChanged);
|
|
122
|
+
return ()=>{
|
|
123
|
+
editSession.removeListener("cellEditChanged", onCellEditChanged);
|
|
124
|
+
};
|
|
125
|
+
}, [
|
|
126
|
+
editSession,
|
|
127
|
+
isCreate,
|
|
128
|
+
rowKey
|
|
129
|
+
]);
|
|
130
|
+
const fail = useCallback((cause, operation)=>{
|
|
131
|
+
const err = toError(cause);
|
|
132
|
+
setError(err);
|
|
133
|
+
reportEditError("useEditForm", callbacksRef.current.onError, err, operation);
|
|
134
|
+
}, []);
|
|
135
|
+
useEffect(()=>{
|
|
136
|
+
if (isCreate && columns) editSession.configureNewRow(columns, requiredColumns);
|
|
137
|
+
}, [
|
|
138
|
+
columns,
|
|
139
|
+
editSession,
|
|
140
|
+
isCreate,
|
|
141
|
+
requiredColumns
|
|
142
|
+
]);
|
|
143
|
+
useEffect(()=>{
|
|
144
|
+
setError(void 0);
|
|
145
|
+
setValidationErrors(EMPTY_ERRORS);
|
|
146
|
+
markRowAdded(false);
|
|
147
|
+
if (!ownsSession) return;
|
|
148
|
+
const transition = isEditMode ? editSession.begin(isCreate ? "Empty" : "Selected") : editSession.end();
|
|
149
|
+
transition.catch((cause)=>fail(cause, isEditMode ? "begin" : "cancel"));
|
|
150
|
+
}, [
|
|
151
|
+
editSession,
|
|
152
|
+
fail,
|
|
153
|
+
isCreate,
|
|
154
|
+
isEditMode,
|
|
155
|
+
markRowAdded,
|
|
156
|
+
ownsSession
|
|
157
|
+
]);
|
|
158
|
+
useEffect(()=>()=>{
|
|
159
|
+
if (isShared) editSession.clearNewRow();
|
|
160
|
+
else if (ownsSession) editSession.end().catch(()=>void 0);
|
|
161
|
+
}, [
|
|
162
|
+
editSession,
|
|
163
|
+
isShared,
|
|
164
|
+
ownsSession
|
|
165
|
+
]);
|
|
166
|
+
const getValues = useCallback(()=>{
|
|
167
|
+
if (isCreate) return editSession.newRowState.values;
|
|
168
|
+
if (isDirect && dataRow) return {
|
|
169
|
+
...getDataRowValues(dataRow),
|
|
170
|
+
...directValuesRef.current
|
|
171
|
+
};
|
|
172
|
+
if (dataRow) return {
|
|
173
|
+
...getDataRowValues(dataRow),
|
|
174
|
+
...editSession.getEditedValues(dataRow.key)
|
|
175
|
+
};
|
|
176
|
+
return {};
|
|
177
|
+
}, [
|
|
178
|
+
dataRow,
|
|
179
|
+
editSession,
|
|
180
|
+
isCreate,
|
|
181
|
+
isDirect
|
|
182
|
+
]);
|
|
183
|
+
const submit = useCallback(async (force = false)=>{
|
|
184
|
+
if (savingRef.current) return false;
|
|
185
|
+
setError(void 0);
|
|
186
|
+
if (isDirect) {
|
|
187
|
+
callbacksRef.current.onSaved?.();
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
if (!rowAddedRef.current) {
|
|
191
|
+
const errors = callbacksRef.current.validate?.(getValues());
|
|
192
|
+
if (hasErrors(errors)) {
|
|
193
|
+
setValidationErrors(errors);
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
setValidationErrors(EMPTY_ERRORS);
|
|
197
|
+
}
|
|
198
|
+
savingRef.current = true;
|
|
199
|
+
setSaving(true);
|
|
200
|
+
try {
|
|
201
|
+
if (isCreate && !rowAddedRef.current) {
|
|
202
|
+
const response = await editSession.addNewRow();
|
|
203
|
+
if (isRpcError(response)) {
|
|
204
|
+
setError(new Error(response.errorMessage));
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
if (hasErrors(editSession.newRowState.errors)) return false;
|
|
208
|
+
if (isShared) {
|
|
209
|
+
callbacksRef.current.onSaved?.();
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
markRowAdded(true);
|
|
213
|
+
}
|
|
214
|
+
await editSession.end(true, force);
|
|
215
|
+
markRowAdded(false);
|
|
216
|
+
callbacksRef.current.onSaved?.();
|
|
217
|
+
return true;
|
|
218
|
+
} catch (cause) {
|
|
219
|
+
fail(cause, "save");
|
|
220
|
+
return false;
|
|
221
|
+
} finally{
|
|
222
|
+
savingRef.current = false;
|
|
223
|
+
setSaving(false);
|
|
224
|
+
}
|
|
225
|
+
}, [
|
|
226
|
+
editSession,
|
|
227
|
+
fail,
|
|
228
|
+
getValues,
|
|
229
|
+
isCreate,
|
|
230
|
+
isDirect,
|
|
231
|
+
isShared,
|
|
232
|
+
markRowAdded
|
|
233
|
+
]);
|
|
234
|
+
const cancel = useCallback(async ()=>{
|
|
235
|
+
try {
|
|
236
|
+
if (isShared) editSession.clearNewRow();
|
|
237
|
+
else if (ownsSession) await editSession.end();
|
|
238
|
+
markRowAdded(false);
|
|
239
|
+
setValidationErrors(EMPTY_ERRORS);
|
|
240
|
+
setError(void 0);
|
|
241
|
+
callbacksRef.current.onCancelled?.();
|
|
242
|
+
} catch (cause) {
|
|
243
|
+
fail(cause, "cancel");
|
|
244
|
+
}
|
|
245
|
+
}, [
|
|
246
|
+
editSession,
|
|
247
|
+
fail,
|
|
248
|
+
isShared,
|
|
249
|
+
markRowAdded,
|
|
250
|
+
ownsSession
|
|
251
|
+
]);
|
|
252
|
+
const setValue = useCallback((name, value)=>{
|
|
253
|
+
editSession.setNewRowValue(name, value);
|
|
254
|
+
setValidationErrors((errors)=>{
|
|
255
|
+
if (void 0 === errors[name]) return errors;
|
|
256
|
+
const { [name]: _removed, ...rest } = errors;
|
|
257
|
+
return rest;
|
|
258
|
+
});
|
|
259
|
+
}, [
|
|
260
|
+
editSession
|
|
261
|
+
]);
|
|
262
|
+
const { errors: newRowErrors, rowErrorColumn } = sessionState.newRowState;
|
|
263
|
+
const fieldErrors = useMemo(()=>{
|
|
264
|
+
if (!isCreate) return validationErrors;
|
|
265
|
+
const { [rowErrorColumn ?? ""]: _rowError, ...errors } = newRowErrors;
|
|
266
|
+
return {
|
|
267
|
+
...errors,
|
|
268
|
+
...validationErrors
|
|
269
|
+
};
|
|
270
|
+
}, [
|
|
271
|
+
isCreate,
|
|
272
|
+
newRowErrors,
|
|
273
|
+
rowErrorColumn,
|
|
274
|
+
validationErrors
|
|
275
|
+
]);
|
|
276
|
+
const isDirty = isCreate ? Object.keys(sessionState.newRowState.values).length > 0 : isDirect ? false : sessionState.isDirty;
|
|
277
|
+
const formDataRow = useMemo(()=>isCreate ? NEW_ROW : dataRow && Object.keys(directValues).length > 0 ? toRowSnapshot(dataRow.key, {
|
|
278
|
+
...getDataRowValues(dataRow),
|
|
279
|
+
...directValues
|
|
280
|
+
}) : dataRow, [
|
|
281
|
+
dataRow,
|
|
282
|
+
directValues,
|
|
283
|
+
isCreate
|
|
284
|
+
]);
|
|
285
|
+
return {
|
|
286
|
+
...sessionState,
|
|
287
|
+
canSave: !saving && (isDirect ? isEditMode : isCreate ? rowAdded ? sessionState.inEditMode : sessionState.isActive && editSession.isNewRowComplete() : sessionState.canSave),
|
|
288
|
+
cancel,
|
|
289
|
+
dataRow: formDataRow,
|
|
290
|
+
editModel: isDirect ? "direct" : "session",
|
|
291
|
+
editSession,
|
|
292
|
+
isSharedSession: isShared,
|
|
293
|
+
fieldEditSession: directEditSession ?? editSession,
|
|
294
|
+
error: error ?? sessionState.error,
|
|
295
|
+
fieldErrors,
|
|
296
|
+
getValues,
|
|
297
|
+
isDirty,
|
|
298
|
+
isEditMode,
|
|
299
|
+
mode,
|
|
300
|
+
rowAdded,
|
|
301
|
+
saving,
|
|
302
|
+
setValue,
|
|
303
|
+
submit
|
|
304
|
+
};
|
|
305
|
+
};
|
|
306
|
+
export { getDataRowValues, useEditForm };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { useCallback, useRef, useSyncExternalStore } from "react";
|
|
2
|
+
const IDLE_STATE = {
|
|
3
|
+
canCancel: false,
|
|
4
|
+
canSave: false,
|
|
5
|
+
editState: "clean",
|
|
6
|
+
inEditMode: false,
|
|
7
|
+
isActive: false,
|
|
8
|
+
isDirty: false,
|
|
9
|
+
isEnding: false,
|
|
10
|
+
isStarting: false,
|
|
11
|
+
lifecycle: {
|
|
12
|
+
status: "idle"
|
|
13
|
+
},
|
|
14
|
+
newRowState: {
|
|
15
|
+
columns: [],
|
|
16
|
+
draftRevision: 0,
|
|
17
|
+
errors: {},
|
|
18
|
+
submitting: false,
|
|
19
|
+
values: {}
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
const createSnapshot = (editSession)=>{
|
|
23
|
+
const { editState, lifecycle, newRowState } = editSession;
|
|
24
|
+
return {
|
|
25
|
+
canCancel: editSession.canCancel,
|
|
26
|
+
canSave: editSession.canSave,
|
|
27
|
+
editState,
|
|
28
|
+
error: "error" === lifecycle.status ? lifecycle.error : void 0,
|
|
29
|
+
inEditMode: editSession.inEditMode,
|
|
30
|
+
isActive: "active" === lifecycle.status,
|
|
31
|
+
isDirty: "clean" !== editState,
|
|
32
|
+
isEnding: "ending" === lifecycle.status,
|
|
33
|
+
isStarting: "starting" === lifecycle.status,
|
|
34
|
+
lifecycle,
|
|
35
|
+
newRowState
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
const useEditSessionState = (editSession)=>{
|
|
39
|
+
const snapshotRef = useRef({
|
|
40
|
+
snapshot: IDLE_STATE
|
|
41
|
+
});
|
|
42
|
+
const subscribe = useCallback((onStoreChange)=>{
|
|
43
|
+
if (!editSession) return ()=>void 0;
|
|
44
|
+
editSession.on("editState", onStoreChange);
|
|
45
|
+
editSession.on("lifecycle", onStoreChange);
|
|
46
|
+
editSession.on("newRow", onStoreChange);
|
|
47
|
+
return ()=>{
|
|
48
|
+
editSession.removeListener("editState", onStoreChange);
|
|
49
|
+
editSession.removeListener("lifecycle", onStoreChange);
|
|
50
|
+
editSession.removeListener("newRow", onStoreChange);
|
|
51
|
+
};
|
|
52
|
+
}, [
|
|
53
|
+
editSession
|
|
54
|
+
]);
|
|
55
|
+
const getSnapshot = useCallback(()=>{
|
|
56
|
+
if (!editSession) return IDLE_STATE;
|
|
57
|
+
const cached = snapshotRef.current;
|
|
58
|
+
const { editState, lifecycle, newRowState } = editSession;
|
|
59
|
+
if (cached.editSession !== editSession || cached.editState !== editState || cached.lifecycle !== lifecycle || cached.newRowState !== newRowState) snapshotRef.current = {
|
|
60
|
+
editSession,
|
|
61
|
+
editState,
|
|
62
|
+
lifecycle,
|
|
63
|
+
newRowState,
|
|
64
|
+
snapshot: createSnapshot(editSession)
|
|
65
|
+
};
|
|
66
|
+
return snapshotRef.current.snapshot;
|
|
67
|
+
}, [
|
|
68
|
+
editSession
|
|
69
|
+
]);
|
|
70
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
71
|
+
};
|
|
72
|
+
export { useEditSessionState };
|
package/src/useEditable.js
CHANGED
|
@@ -1,19 +1,31 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef } from "react";
|
|
1
2
|
import { EditSession } from "./EditSession.js";
|
|
2
3
|
import { useEditMode } from "./EditModeProvider.js";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
import { reportEditError } from "./edit-errors.js";
|
|
5
|
+
import { useEditSessionState } from "./useEditSessionState.js";
|
|
6
|
+
const useEditable = ({ copyOption = "Selected", dataSource, deleteMode, editSessionApi, isEditMode: isEditModeProp, onCancel, onError, onSave, rowDefaults })=>{
|
|
7
|
+
const { isEditMode: isEditModeContext } = useEditMode();
|
|
8
|
+
const isEditMode = isEditModeProp ?? isEditModeContext;
|
|
9
|
+
const onErrorRef = useRef(onError);
|
|
10
|
+
onErrorRef.current = onError;
|
|
6
11
|
const editSession = useMemo(()=>new EditSession({
|
|
7
|
-
dataSource
|
|
12
|
+
dataSource,
|
|
13
|
+
deleteMode,
|
|
14
|
+
editSessionApi,
|
|
15
|
+
rowDefaults
|
|
8
16
|
}), [
|
|
9
|
-
dataSource
|
|
17
|
+
dataSource,
|
|
18
|
+
deleteMode,
|
|
19
|
+
editSessionApi,
|
|
20
|
+
rowDefaults
|
|
10
21
|
]);
|
|
22
|
+
const sessionState = useEditSessionState(editSession);
|
|
11
23
|
const handleCancel = useCallback(async ()=>{
|
|
12
24
|
try {
|
|
13
25
|
await editSession.end();
|
|
14
26
|
onCancel();
|
|
15
27
|
} catch (error) {
|
|
16
|
-
|
|
28
|
+
reportEditError("useEditable", onErrorRef.current, error, "cancel");
|
|
17
29
|
}
|
|
18
30
|
}, [
|
|
19
31
|
editSession,
|
|
@@ -24,7 +36,7 @@ const useEditable = ({ copyOption = "Selected", dataSource, onCancel, onSave })=
|
|
|
24
36
|
await editSession.end(true, force);
|
|
25
37
|
onSave();
|
|
26
38
|
} catch (error) {
|
|
27
|
-
|
|
39
|
+
reportEditError("useEditable", onErrorRef.current, error, "save");
|
|
28
40
|
}
|
|
29
41
|
}, [
|
|
30
42
|
editSession,
|
|
@@ -34,9 +46,9 @@ const useEditable = ({ copyOption = "Selected", dataSource, onCancel, onSave })=
|
|
|
34
46
|
const transition = isEditMode ? editSession.begin(copyOption) : editSession.end();
|
|
35
47
|
transition.catch((error)=>{
|
|
36
48
|
if (isEditMode) {
|
|
37
|
-
|
|
49
|
+
reportEditError("useEditable", onErrorRef.current, error, "begin");
|
|
38
50
|
handleCancel();
|
|
39
|
-
} else
|
|
51
|
+
} else reportEditError("useEditable", onErrorRef.current, error, "cancel");
|
|
40
52
|
});
|
|
41
53
|
}, [
|
|
42
54
|
copyOption,
|
|
@@ -45,7 +57,12 @@ const useEditable = ({ copyOption = "Selected", dataSource, onCancel, onSave })=
|
|
|
45
57
|
handleCancel
|
|
46
58
|
]);
|
|
47
59
|
return {
|
|
60
|
+
canCancel: sessionState.canCancel,
|
|
61
|
+
canSave: sessionState.canSave,
|
|
48
62
|
editSession,
|
|
63
|
+
editState: sessionState.editState,
|
|
64
|
+
isEditMode,
|
|
65
|
+
lifecycle: sessionState.lifecycle,
|
|
49
66
|
onCancel: handleCancel,
|
|
50
67
|
onSave: handleSave
|
|
51
68
|
};
|