@vuu-ui/vuu-data-react 2.1.19-beta.1 → 2.1.19-beta.2
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 +16 -17
- package/src/data-editing/EditForm.css.js +53 -0
- package/src/data-editing/EditForm.js +82 -0
- package/src/data-editing/UnsavedChangesReport.css.js +27 -0
- package/src/data-editing/UnsavedChangesReport.js +41 -0
- package/src/data-editing/edit-rule-validation-checker.js +30 -0
- package/src/data-editing/edit-validation-rules.js +33 -0
- package/src/data-editing/form-edit-state.js +16 -0
- package/src/data-editing/get-data-item-edit-control.js +63 -0
- package/src/data-editing/index.js +6 -0
- package/src/data-editing/useEditForm.js +253 -0
- package/src/datasource-provider/VuuDataSourceProvider.js +20 -0
- package/src/datasource-provider/index.js +1 -0
- package/src/datasource-provider/useAutoLoginToVuuServer.js +42 -0
- package/src/hooks/useLookupValues.js +93 -0
- package/src/hooks/useSessionDataSource.js +37 -0
- package/src/hooks/useTypeaheadSuggestions.js +50 -0
- package/src/hooks/useVisualLinks.js +87 -0
- package/src/hooks/useVuuMenuActions.js +269 -0
- package/src/hooks/useVuuTables.js +32 -0
- package/src/index.js +10 -0
- package/src/lost-connection-indicator/LostConnectionIndicator.css.js +29 -0
- package/src/lost-connection-indicator/LostConnectionIndicator.js +30 -0
- package/src/lost-connection-indicator/useLostConnection.js +28 -0
- package/src/session-editing-form/SessionEditingForm.css.js +47 -0
- package/src/session-editing-form/SessionEditingForm.js +236 -0
- package/src/session-editing-form/index.js +1 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useComponentCssInjection } from "@salt-ds/styles";
|
|
3
|
+
import { useWindow } from "@salt-ds/window";
|
|
4
|
+
import LostConnectionIndicator from "./LostConnectionIndicator.css";
|
|
5
|
+
const classBase = "vuuLostConnectionIndicator";
|
|
6
|
+
const LostConnectionIndicator_LostConnectionIndicator = ()=>{
|
|
7
|
+
const targetWindow = useWindow();
|
|
8
|
+
useComponentCssInjection({
|
|
9
|
+
testId: "vuu-toast-notification",
|
|
10
|
+
css: LostConnectionIndicator,
|
|
11
|
+
window: targetWindow
|
|
12
|
+
});
|
|
13
|
+
return /*#__PURE__*/ jsxs("div", {
|
|
14
|
+
className: classBase,
|
|
15
|
+
children: [
|
|
16
|
+
/*#__PURE__*/ jsx("h2", {
|
|
17
|
+
className: `${classBase}-title`,
|
|
18
|
+
children: "CONNECTION LOST"
|
|
19
|
+
}),
|
|
20
|
+
/*#__PURE__*/ jsx("div", {
|
|
21
|
+
className: `${classBase}-spinner`
|
|
22
|
+
}),
|
|
23
|
+
/*#__PURE__*/ jsx("h3", {
|
|
24
|
+
className: `${classBase}-status`,
|
|
25
|
+
children: "RECONNECTING"
|
|
26
|
+
})
|
|
27
|
+
]
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
export { LostConnectionIndicator_LostConnectionIndicator as LostConnectionIndicator };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { ConnectionManager, isConnected } from "@vuu-ui/vuu-data-remote";
|
|
3
|
+
import { useCallback, useMemo, useRef } from "react";
|
|
4
|
+
import { NotificationType, useNotifications } from "@vuu-ui/vuu-notifications";
|
|
5
|
+
import { LostConnectionIndicator } from "./LostConnectionIndicator.js";
|
|
6
|
+
const useLostConnection = ()=>{
|
|
7
|
+
const { hideNotification, showNotification } = useNotifications();
|
|
8
|
+
const isConnectedRef = useRef(ConnectionManager.connected);
|
|
9
|
+
const handleConnectionStatusChange = useCallback((connectionStatus)=>{
|
|
10
|
+
const { current: wasConnected } = isConnectedRef;
|
|
11
|
+
isConnectedRef.current = isConnected(connectionStatus);
|
|
12
|
+
if (wasConnected && "disconnected" === connectionStatus) showNotification({
|
|
13
|
+
content: /*#__PURE__*/ jsx(LostConnectionIndicator, {}),
|
|
14
|
+
status: "error",
|
|
15
|
+
type: NotificationType.Workspace
|
|
16
|
+
});
|
|
17
|
+
else if (!wasConnected && isConnectedRef.current) hideNotification();
|
|
18
|
+
}, [
|
|
19
|
+
hideNotification,
|
|
20
|
+
showNotification
|
|
21
|
+
]);
|
|
22
|
+
useMemo(async ()=>{
|
|
23
|
+
ConnectionManager.on("connection-status", handleConnectionStatusChange);
|
|
24
|
+
}, [
|
|
25
|
+
handleConnectionStatusChange
|
|
26
|
+
]);
|
|
27
|
+
};
|
|
28
|
+
export { useLostConnection };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const css = `
|
|
2
|
+
.vuuSessionEditingForm {
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: 3px;
|
|
5
|
+
min-width: 400px;
|
|
6
|
+
display: flex;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.vuuSessionEditingForm-content {
|
|
10
|
+
padding: var(--salt-spacing-200);
|
|
11
|
+
flex-direction: column;
|
|
12
|
+
flex: auto;
|
|
13
|
+
gap: 3px;
|
|
14
|
+
display: flex;
|
|
15
|
+
overflow: auto;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.vuuSessionEditingForm-fieldValue.vuuReadOnly {
|
|
19
|
+
font-weight: var(--salt-text-label-fontWeight-strong);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.vuuSessionEditingForm-buttonbar {
|
|
23
|
+
border-top: solid 1px var(--salt-container-primary-borderColor);
|
|
24
|
+
box-sizing: content-box;
|
|
25
|
+
flex: 0 0 autox;
|
|
26
|
+
height: var(--salt-size-base);
|
|
27
|
+
margin: var(--salt-spacing-200);
|
|
28
|
+
padding-top: var(--salt-spacing-200);
|
|
29
|
+
justify-content: flex-end;
|
|
30
|
+
align-items: center;
|
|
31
|
+
gap: 6px;
|
|
32
|
+
display: flex;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.vuuSessionEditingForm-errorBanner {
|
|
36
|
+
--vuu-icon-left: 3px;
|
|
37
|
+
--vuu-icon-size: 18px;
|
|
38
|
+
--vuu-icon-top: 3px;
|
|
39
|
+
border: solid 1px var(--salt-status-error-borderColor);
|
|
40
|
+
padding: 0 6px 0 26px;
|
|
41
|
+
line-height: 24px;
|
|
42
|
+
position: relative;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
`;
|
|
47
|
+
export default css;
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { VuuDataSource } from "@vuu-ui/vuu-data-remote";
|
|
3
|
+
import { Range, buildColumnMap, getTypedValue, queryClosest, shallowEquals } from "@vuu-ui/vuu-utils";
|
|
4
|
+
import { Button, FormField, FormFieldHelperText, FormFieldLabel, Input, useIdMemo } from "@salt-ds/core";
|
|
5
|
+
import { useComponentCssInjection } from "@salt-ds/styles";
|
|
6
|
+
import { useWindow } from "@salt-ds/window";
|
|
7
|
+
import clsx from "clsx";
|
|
8
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
9
|
+
import SessionEditingForm from "./SessionEditingForm.css";
|
|
10
|
+
const classBase = "vuuSessionEditingForm";
|
|
11
|
+
const getField = (fields, name)=>{
|
|
12
|
+
const field = fields.find((f)=>f.name === name);
|
|
13
|
+
if (field) return field;
|
|
14
|
+
throw Error(`SessionEditingForm, no field '${name}' found`);
|
|
15
|
+
};
|
|
16
|
+
const getFieldNameAndValue = ({ target })=>{
|
|
17
|
+
const formField = queryClosest(target, ".saltFormField");
|
|
18
|
+
if (formField) {
|
|
19
|
+
const { dataset: { field } } = formField;
|
|
20
|
+
if (void 0 === field) throw Error("SessionEditingForm, form field has no field data attribute");
|
|
21
|
+
return [
|
|
22
|
+
field,
|
|
23
|
+
target.value
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
throw Error("Form control is not enclosed in FormField");
|
|
27
|
+
};
|
|
28
|
+
const Status = {
|
|
29
|
+
uninitialised: 0,
|
|
30
|
+
unchanged: 1,
|
|
31
|
+
changed: 2,
|
|
32
|
+
invalid: 3
|
|
33
|
+
};
|
|
34
|
+
const getDataSource = (dataSource, schema)=>{
|
|
35
|
+
if (dataSource) return dataSource;
|
|
36
|
+
if (schema) return new VuuDataSource({
|
|
37
|
+
bufferSize: 0,
|
|
38
|
+
table: schema.table,
|
|
39
|
+
columns: schema.columns.map((col)=>col.name)
|
|
40
|
+
});
|
|
41
|
+
throw Error("SessionEditingForm: either a DataSource or a TableSchema must be provided");
|
|
42
|
+
};
|
|
43
|
+
const SessionEditingForm_SessionEditingForm = ({ className, config: { fields, key: keyField }, dataSource: dataSourceProp, id: idProp, onClose, schema, ...htmlAttributes })=>{
|
|
44
|
+
const targetWindow = useWindow();
|
|
45
|
+
useComponentCssInjection({
|
|
46
|
+
testId: "vuu-session-editing-form",
|
|
47
|
+
css: SessionEditingForm,
|
|
48
|
+
window: targetWindow
|
|
49
|
+
});
|
|
50
|
+
const [fieldStatusValues, setFieldStatusValues] = useState({});
|
|
51
|
+
const [values, setValues] = useState();
|
|
52
|
+
const [errorMessage, setErrorMessage] = useState("");
|
|
53
|
+
const formContentRef = useRef(null);
|
|
54
|
+
const initialDataRef = useRef(void 0);
|
|
55
|
+
const dataStatusRef = useRef(Status.uninitialised);
|
|
56
|
+
const dataSource = useMemo(()=>{
|
|
57
|
+
const ds = getDataSource(dataSourceProp, schema);
|
|
58
|
+
const { columns } = ds;
|
|
59
|
+
const columnMap = buildColumnMap(ds.columns);
|
|
60
|
+
const applyServerData = (data)=>{
|
|
61
|
+
if (columnMap) {
|
|
62
|
+
const values = {};
|
|
63
|
+
for (const column of columns)values[column] = data[columnMap[column]];
|
|
64
|
+
if (dataStatusRef.current === Status.uninitialised) {
|
|
65
|
+
dataStatusRef.current = Status.unchanged;
|
|
66
|
+
initialDataRef.current = values;
|
|
67
|
+
}
|
|
68
|
+
setValues(values);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
ds.subscribe({
|
|
72
|
+
range: Range(0, 5)
|
|
73
|
+
}, (message)=>{
|
|
74
|
+
if ("viewport-update" === message.type && message.rows) if (dataStatusRef.current === Status.uninitialised) applyServerData(message.rows[0]);
|
|
75
|
+
else console.log("what do we do with server updates");
|
|
76
|
+
});
|
|
77
|
+
return ds;
|
|
78
|
+
}, [
|
|
79
|
+
dataSourceProp,
|
|
80
|
+
schema
|
|
81
|
+
]);
|
|
82
|
+
const id = useIdMemo(idProp);
|
|
83
|
+
const handleChange = useCallback((evt)=>{
|
|
84
|
+
const [field, value] = getFieldNameAndValue(evt);
|
|
85
|
+
const { type } = getField(fields, field);
|
|
86
|
+
const typedValue = getTypedValue(value, type);
|
|
87
|
+
setValues((values = {})=>{
|
|
88
|
+
const newValues = {
|
|
89
|
+
...values,
|
|
90
|
+
[field]: typedValue
|
|
91
|
+
};
|
|
92
|
+
const notUpdated = shallowEquals(newValues, initialDataRef.current);
|
|
93
|
+
dataStatusRef.current = notUpdated ? Status.unchanged : void 0 !== typedValue ? Status.changed : Status.invalid;
|
|
94
|
+
return newValues;
|
|
95
|
+
});
|
|
96
|
+
}, [
|
|
97
|
+
fields
|
|
98
|
+
]);
|
|
99
|
+
const handleBlur = useCallback(async (evt)=>{
|
|
100
|
+
const [column, value] = getFieldNameAndValue(evt);
|
|
101
|
+
const key = values?.[keyField];
|
|
102
|
+
const { type } = getField(fields, column);
|
|
103
|
+
const data = getTypedValue(value, type, true);
|
|
104
|
+
if ("string" == typeof key) {
|
|
105
|
+
const response = await dataSource.rpcRequest?.({
|
|
106
|
+
params: {
|
|
107
|
+
key,
|
|
108
|
+
column,
|
|
109
|
+
data
|
|
110
|
+
},
|
|
111
|
+
rpcName: "editCell",
|
|
112
|
+
type: "RPC_REQUEST"
|
|
113
|
+
});
|
|
114
|
+
if (response?.type === "SUCCESS_RESULT") setFieldStatusValues((map)=>({
|
|
115
|
+
...map,
|
|
116
|
+
[column]: void 0
|
|
117
|
+
}));
|
|
118
|
+
else if (response?.type === "ERROR_RESULT") {
|
|
119
|
+
console.log(`edit rejected ${response.errorMessage}`);
|
|
120
|
+
setFieldStatusValues((map)=>({
|
|
121
|
+
...map,
|
|
122
|
+
[column]: response.errorMessage
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}, [
|
|
127
|
+
dataSource,
|
|
128
|
+
fields,
|
|
129
|
+
keyField,
|
|
130
|
+
values
|
|
131
|
+
]);
|
|
132
|
+
const handleSubmit = useCallback(async ()=>{
|
|
133
|
+
const response = await dataSource.rpcRequest?.({
|
|
134
|
+
params: {
|
|
135
|
+
comment: ""
|
|
136
|
+
},
|
|
137
|
+
rpcName: "submitForm",
|
|
138
|
+
type: "RPC_REQUEST"
|
|
139
|
+
});
|
|
140
|
+
if (response?.type === "SUCCESS_RESULT") onClose?.();
|
|
141
|
+
else if (response?.type === "ERROR_RESULT") setErrorMessage(response.errorMessage);
|
|
142
|
+
}, [
|
|
143
|
+
dataSource,
|
|
144
|
+
onClose
|
|
145
|
+
]);
|
|
146
|
+
const handleKeyDown = useCallback((evt)=>{
|
|
147
|
+
if ("Enter" === evt.key && dataStatusRef.current === Status.changed) handleSubmit();
|
|
148
|
+
}, [
|
|
149
|
+
handleSubmit
|
|
150
|
+
]);
|
|
151
|
+
const handleCancel = useCallback(()=>{
|
|
152
|
+
onClose?.();
|
|
153
|
+
}, [
|
|
154
|
+
onClose
|
|
155
|
+
]);
|
|
156
|
+
const getFormControl = (field)=>{
|
|
157
|
+
const value = String(values?.[field.name] ?? "");
|
|
158
|
+
if (field.readonly || field.name === keyField) return /*#__PURE__*/ jsx("div", {
|
|
159
|
+
className: `${classBase}-fieldValue vuuReadOnly`,
|
|
160
|
+
children: value
|
|
161
|
+
});
|
|
162
|
+
return /*#__PURE__*/ jsx(Input, {
|
|
163
|
+
className: `${classBase}-fieldValue`,
|
|
164
|
+
onBlur: handleBlur,
|
|
165
|
+
onChange: handleChange,
|
|
166
|
+
value: value,
|
|
167
|
+
id: `${id}-input-${field.name}`
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
useEffect(()=>{
|
|
171
|
+
if (formContentRef.current) {
|
|
172
|
+
const firstInput = formContentRef.current.querySelector("input");
|
|
173
|
+
if (firstInput) setTimeout(()=>{
|
|
174
|
+
firstInput.focus();
|
|
175
|
+
firstInput.select();
|
|
176
|
+
}, 100);
|
|
177
|
+
}
|
|
178
|
+
}, []);
|
|
179
|
+
useEffect(()=>()=>{
|
|
180
|
+
if (dataSource) dataSource.unsubscribe();
|
|
181
|
+
}, [
|
|
182
|
+
dataSource
|
|
183
|
+
]);
|
|
184
|
+
const isDirty = dataStatusRef.current === Status.changed;
|
|
185
|
+
return /*#__PURE__*/ jsxs("div", {
|
|
186
|
+
...htmlAttributes,
|
|
187
|
+
className: clsx(classBase, className),
|
|
188
|
+
children: [
|
|
189
|
+
errorMessage ? /*#__PURE__*/ jsx("div", {
|
|
190
|
+
className: `${classBase}-errorBanner`,
|
|
191
|
+
"data-icon": "error",
|
|
192
|
+
title: errorMessage,
|
|
193
|
+
children: "Error, edit(s) not saved"
|
|
194
|
+
}) : void 0,
|
|
195
|
+
/*#__PURE__*/ jsx("div", {
|
|
196
|
+
className: `${classBase}-content`,
|
|
197
|
+
ref: formContentRef,
|
|
198
|
+
onKeyDown: handleKeyDown,
|
|
199
|
+
children: fields.map((field)=>/*#__PURE__*/ jsxs(FormField, {
|
|
200
|
+
className: `${classBase}-field`,
|
|
201
|
+
"data-field": field.name,
|
|
202
|
+
necessity: field.required ? "required" : "optional",
|
|
203
|
+
readOnly: field.readonly,
|
|
204
|
+
validationStatus: fieldStatusValues[field.name] ? "error" : void 0,
|
|
205
|
+
children: [
|
|
206
|
+
/*#__PURE__*/ jsx(FormFieldLabel, {
|
|
207
|
+
children: field?.label ?? field.description
|
|
208
|
+
}),
|
|
209
|
+
getFormControl(field),
|
|
210
|
+
/*#__PURE__*/ jsx(FormFieldHelperText, {
|
|
211
|
+
children: fieldStatusValues[field.name] ?? ""
|
|
212
|
+
})
|
|
213
|
+
]
|
|
214
|
+
}, field.name))
|
|
215
|
+
}),
|
|
216
|
+
/*#__PURE__*/ jsxs("div", {
|
|
217
|
+
className: `${classBase}-buttonbar salt-theme salt-density-high`,
|
|
218
|
+
children: [
|
|
219
|
+
/*#__PURE__*/ jsx(Button, {
|
|
220
|
+
type: "submit",
|
|
221
|
+
variant: "cta",
|
|
222
|
+
disabled: !isDirty,
|
|
223
|
+
onClick: handleSubmit,
|
|
224
|
+
children: "Submit"
|
|
225
|
+
}),
|
|
226
|
+
/*#__PURE__*/ jsx(Button, {
|
|
227
|
+
variant: "secondary",
|
|
228
|
+
onClick: handleCancel,
|
|
229
|
+
children: "Cancel"
|
|
230
|
+
})
|
|
231
|
+
]
|
|
232
|
+
})
|
|
233
|
+
]
|
|
234
|
+
});
|
|
235
|
+
};
|
|
236
|
+
export { SessionEditingForm_SessionEditingForm as SessionEditingForm };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./SessionEditingForm.js";
|