@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
package/README.md
CHANGED
|
@@ -4,6 +4,41 @@ Shared data editing APIs, state management, and React components for VUU UI.
|
|
|
4
4
|
|
|
5
5
|
This package coordinates edit-session state with editable data sources (both in-memory and remote Vuu session tables) and provides React hooks and UI controls for table editing.
|
|
6
6
|
|
|
7
|
+
## Quick start
|
|
8
|
+
|
|
9
|
+
Most applications don't need to assemble the pieces in this package by hand.
|
|
10
|
+
`@vuu-ui/vuu-table-extras` provides ready-made patterns built on it:
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { EditableTable } from "@vuu-ui/vuu-table-extras";
|
|
14
|
+
|
|
15
|
+
// Edit options live on the column descriptors, e.g. { name: "ric", editable: false }
|
|
16
|
+
<EditableTable
|
|
17
|
+
allowDelete
|
|
18
|
+
config={{ columns }}
|
|
19
|
+
dataSource={dataSource}
|
|
20
|
+
showInlineAddRow
|
|
21
|
+
/>
|
|
22
|
+
|
|
23
|
+
// The selected row in a generated form beside the table
|
|
24
|
+
<EditableTable config={{ columns }} dataSource={dataSource} editStrategy="form" />
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
See the Data Editing guides in the showcase (_Docs / DataEditing_, source in
|
|
28
|
+
[`showcase/src/examples/Docs/DataEditing`](../../showcase/src/examples/Docs/DataEditing/)), starting
|
|
29
|
+
with [Getting started](../../showcase/src/examples/Docs/DataEditing/01-GettingStarted.mdx), and the
|
|
30
|
+
_DataEditing_ examples in the showcase.
|
|
31
|
+
|
|
32
|
+
## Layers
|
|
33
|
+
|
|
34
|
+
| Layer | Use it when | APIs |
|
|
35
|
+
| ----- | ----------- | ---- |
|
|
36
|
+
| Pattern components (`@vuu-ui/vuu-table-extras`) | A standard editing UI fits. | `EditableTable` (inline or form editing), `useBulkEditDialog` |
|
|
37
|
+
| Composable hooks and components (this package) | You need your own layout or behaviour. | `useEditableTable`, `useEditableColumns`, `useEditForm`, `EditForm`, `CreateRowForm`, `EditButtons`, `useConfirmDiscard`, `useEditSessionState`, `useLookupOptions` |
|
|
38
|
+
| Primitives (this package) | You are building a new pattern. | `EditSession`, `DirectEditSession`, `DataEditingProvider`, `EditModeProvider` |
|
|
39
|
+
|
|
40
|
+
The rest of this document describes the primitives and how they work.
|
|
41
|
+
|
|
7
42
|
---
|
|
8
43
|
|
|
9
44
|
## Table of Contents
|
|
@@ -148,13 +183,14 @@ A cell is considered edited only when `originalValue !== editedValue && isValid
|
|
|
148
183
|
### New Row Flow (`addNewRow` / `addRow`)
|
|
149
184
|
|
|
150
185
|
1. **Configure**: `editSession.configureNewRow(columns, requiredColumns)` defines the expected column inputs.
|
|
151
|
-
2. **Draft**: `editSession.setNewRowValue(column, value)` updates `#newRowState.values`.
|
|
152
|
-
3. **Submit**: `editSession.addNewRow()` validates required columns and
|
|
186
|
+
2. **Draft**: `editSession.setNewRowValue(column, value)` updates `#newRowState.values` (or removes the column entry if cleared to `undefined`/`""`) and clears any recorded error for `column`. `editSession.setNewRowError(column, errorMessage)` records a validation error in `#newRowState.errors` and `editSession.clearNewRowError(column)` clears it on cancel.
|
|
187
|
+
3. **Submit**: `editSession.addNewRow()` validates required columns and checks that `#newRowState.errors` is empty before delegating to `addRow()`.
|
|
153
188
|
4. **`addRow(rowData)`**:
|
|
154
189
|
- Calls `dataSource.addRow(rowData)` with `#rowDefaults` merged in.
|
|
155
190
|
- On success, extracts the row key directly from the response map (`response.data.key`) and records it in `#addedRowKeys`.
|
|
156
191
|
- Increments `addCount`.
|
|
157
192
|
- Resets new-row form draft and increments `draftRevision`.
|
|
193
|
+
5. **Rejection**: if the server rejects the row, the draft is kept and the message is stored in `errors` under `rowErrorColumn` (the final column, where an inline add row shows it). Forms show it in their error banner. The next change to the draft clears it.
|
|
158
194
|
|
|
159
195
|
### Row Deletions (`deleteSelectedRows`)
|
|
160
196
|
|
|
@@ -190,8 +226,9 @@ editSession.undoRowChange(key, action);
|
|
|
190
226
|
|
|
191
227
|
- **`end(saveChanges = false, force = false)`**:
|
|
192
228
|
- Transitions lifecycle to `"ending"`.
|
|
193
|
-
- Calls `dataSource.endEditSession(saveChanges, force)
|
|
229
|
+
- Calls `dataSource.endEditSession(saveChanges, force)` (skipped when discarding an already-unsubscribed session DataSource).
|
|
194
230
|
- On success: clears all edits, resets counters, unsubscribes session DataSource, transitions to `"idle"`.
|
|
231
|
+
- **Session Disconnection / Unsubscribe**: When a WebSocket disconnects, `ServerProxy` drops session-table viewports and posts `subscribe-failed`. The session DataSource emits `"unsubscribed"`, `EditSession` automatically ends the session locally (`end(false)`) without issuing `endEditSession`, and `useEditableTable` invokes `onCancel` to exit edit mode back to view mode.
|
|
195
232
|
- **Stale Updates (`StaleUpdateError`)**: If the server rejects the commit due to concurrent edits, lifecycle transitions to `error { operation: "end" }` and `editState` becomes `"stale"`. The user can force-save with `end(true, true)`.
|
|
196
233
|
|
|
197
234
|
---
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "5.0.0-alpha.
|
|
2
|
+
"version": "5.0.0-alpha.4",
|
|
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,14 @@
|
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@salt-ds/core": "1.74.0",
|
|
15
|
-
"@vuu-ui/
|
|
16
|
-
"@vuu-ui/vuu-
|
|
15
|
+
"@vuu-ui/core": "5.0.0-alpha.4",
|
|
16
|
+
"@vuu-ui/vuu-ui-controls": "5.0.0-alpha.4",
|
|
17
|
+
"@vuu-ui/vuu-utils": "5.0.0-alpha.4"
|
|
17
18
|
},
|
|
18
19
|
"devDependencies": {
|
|
19
|
-
"@vuu-ui/vuu-data-types": "5.0.0-alpha.
|
|
20
|
-
"@vuu-ui/vuu-protocol-types": "5.0.0-alpha.
|
|
21
|
-
"@vuu-ui/vuu-table-types": "5.0.0-alpha.
|
|
20
|
+
"@vuu-ui/vuu-data-types": "5.0.0-alpha.4",
|
|
21
|
+
"@vuu-ui/vuu-protocol-types": "5.0.0-alpha.4",
|
|
22
|
+
"@vuu-ui/vuu-table-types": "5.0.0-alpha.4"
|
|
22
23
|
},
|
|
23
24
|
"peerDependencies": {
|
|
24
25
|
"react": "^19.2.3"
|
package/src/DirectEditSession.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { EventEmitter } from "@vuu-ui/vuu-utils";
|
|
2
2
|
class DirectEditSession extends EventEmitter {
|
|
3
3
|
#dataSource;
|
|
4
|
-
|
|
4
|
+
#validate;
|
|
5
|
+
constructor({ dataSource, validate }){
|
|
5
6
|
super();
|
|
6
7
|
this.#dataSource = dataSource;
|
|
8
|
+
this.#validate = validate;
|
|
7
9
|
}
|
|
8
10
|
get dataSource() {
|
|
9
11
|
return this.#dataSource;
|
|
@@ -25,6 +27,11 @@ class DirectEditSession extends EventEmitter {
|
|
|
25
27
|
errorMessage: "Invalid value",
|
|
26
28
|
type: "ERROR_RESULT"
|
|
27
29
|
};
|
|
30
|
+
const errorMessage = this.#validate?.(key, columnName, typedValue);
|
|
31
|
+
if (errorMessage) return {
|
|
32
|
+
errorMessage,
|
|
33
|
+
type: "ERROR_RESULT"
|
|
34
|
+
};
|
|
28
35
|
const editCell = this.#dataSource.editCell;
|
|
29
36
|
if (void 0 === editCell) throw Error("[DirectEditSession] datasource does not support editCell");
|
|
30
37
|
const response = await editCell.call(this.#dataSource, key, columnName, typedValue);
|
package/src/EditButtons.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Button } from "@salt-ds/core";
|
|
3
3
|
import { useCallback } from "react";
|
|
4
|
-
import {
|
|
5
|
-
const EditButtons = ({ canCancel, canSave:
|
|
6
|
-
const editState =
|
|
7
|
-
const
|
|
4
|
+
import { useEditSessionState } from "./useEditSessionState.js";
|
|
5
|
+
const EditButtons = ({ canCancel: canCancelProp, canSave: canSaveProp, confirmCancel, confirmSave, editSession, hasSelection = false, onCancel, onDelete, onSave, saveLabel = "Save" })=>{
|
|
6
|
+
const { canCancel: sessionCanCancel, canSave: sessionCanSave, editState, isDirty } = useEditSessionState(editSession);
|
|
7
|
+
const canSave = false !== canSaveProp && sessionCanSave;
|
|
8
|
+
const canCancel = canCancelProp ?? sessionCanCancel;
|
|
8
9
|
const handleSave = useCallback(async ()=>{
|
|
9
10
|
if (confirmSave) {
|
|
10
11
|
const confirmed = await confirmSave();
|
|
@@ -36,7 +37,7 @@ const EditButtons = ({ canCancel, canSave: _canSave, confirmCancel, confirmSave,
|
|
|
36
37
|
children: "Delete"
|
|
37
38
|
}),
|
|
38
39
|
/*#__PURE__*/ jsx(Button, {
|
|
39
|
-
disabled: !
|
|
40
|
+
disabled: !canSave,
|
|
40
41
|
onClick: handleSave,
|
|
41
42
|
sentiment: "accented",
|
|
42
43
|
children: "stale" === editState ? `${saveLabel} (force)` : saveLabel
|
package/src/EditModeProvider.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
|
-
import { createContext, useContext, useEffect, useState } from "react";
|
|
2
|
+
import { createContext, useContext, useEffect, useMemo, useState } from "react";
|
|
3
|
+
let warnedMissingProvider = false;
|
|
3
4
|
const EditModeContext = /*#__PURE__*/ createContext({
|
|
4
5
|
isEditMode: false,
|
|
5
|
-
setEditMode: ()=>
|
|
6
|
+
setEditMode: ()=>{
|
|
7
|
+
if ("production" !== process.env.NODE_ENV && !warnedMissingProvider) {
|
|
8
|
+
warnedMissingProvider = true;
|
|
9
|
+
console.warn("[useEditMode] setEditMode called with no EditModeProvider in scope, edit mode will not change");
|
|
10
|
+
}
|
|
11
|
+
}
|
|
6
12
|
});
|
|
7
13
|
const EditModeProvider = ({ children, isEditMode: isEditModeProp = false })=>{
|
|
8
14
|
const [isEditMode, setEditMode] = useState(isEditModeProp);
|
|
@@ -11,11 +17,14 @@ const EditModeProvider = ({ children, isEditMode: isEditModeProp = false })=>{
|
|
|
11
17
|
}, [
|
|
12
18
|
isEditModeProp
|
|
13
19
|
]);
|
|
14
|
-
|
|
15
|
-
value: {
|
|
20
|
+
const value = useMemo(()=>({
|
|
16
21
|
isEditMode,
|
|
17
22
|
setEditMode
|
|
18
|
-
},
|
|
23
|
+
}), [
|
|
24
|
+
isEditMode
|
|
25
|
+
]);
|
|
26
|
+
return /*#__PURE__*/ jsx(EditModeContext.Provider, {
|
|
27
|
+
value: value,
|
|
19
28
|
children: children
|
|
20
29
|
});
|
|
21
30
|
};
|
package/src/EditSession.js
CHANGED
|
@@ -123,6 +123,8 @@ class EditSession extends EventEmitter {
|
|
|
123
123
|
return this.#newRowState.columns.at(-1) === columnName;
|
|
124
124
|
}
|
|
125
125
|
isNewRowComplete() {
|
|
126
|
+
const { errors, rowErrorColumn } = this.#newRowState;
|
|
127
|
+
if (Object.keys(errors).some((column)=>column !== rowErrorColumn)) return false;
|
|
126
128
|
const requiredColumns = this.#newRowState.requiredColumns ?? this.#newRowState.columns;
|
|
127
129
|
return requiredColumns.every((column)=>{
|
|
128
130
|
const value = this.#newRowState.values[column];
|
|
@@ -145,17 +147,61 @@ class EditSession extends EventEmitter {
|
|
|
145
147
|
});
|
|
146
148
|
}
|
|
147
149
|
setNewRowValue(column, value) {
|
|
150
|
+
const { rowErrorColumn, ...newRowState } = this.#newRowState;
|
|
151
|
+
const errors = {
|
|
152
|
+
...newRowState.errors
|
|
153
|
+
};
|
|
154
|
+
delete errors[column];
|
|
155
|
+
if (void 0 !== rowErrorColumn) delete errors[rowErrorColumn];
|
|
156
|
+
const values = {
|
|
157
|
+
...newRowState.values
|
|
158
|
+
};
|
|
159
|
+
if (void 0 === value || "string" == typeof value && "" === value.trim()) delete values[column];
|
|
160
|
+
else values[column] = value;
|
|
161
|
+
this.#setNewRowState({
|
|
162
|
+
...newRowState,
|
|
163
|
+
errors,
|
|
164
|
+
values
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
setNewRowError(column, errorMessage = "Invalid value") {
|
|
168
|
+
const { rowErrorColumn, ...newRowState } = this.#newRowState;
|
|
169
|
+
const errors = {
|
|
170
|
+
...newRowState.errors
|
|
171
|
+
};
|
|
172
|
+
if (void 0 !== rowErrorColumn) delete errors[rowErrorColumn];
|
|
173
|
+
const values = {
|
|
174
|
+
...newRowState.values
|
|
175
|
+
};
|
|
176
|
+
delete values[column];
|
|
177
|
+
this.#setNewRowState({
|
|
178
|
+
...newRowState,
|
|
179
|
+
errors: {
|
|
180
|
+
...errors,
|
|
181
|
+
[column]: errorMessage
|
|
182
|
+
},
|
|
183
|
+
values
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
clearNewRowError(column) {
|
|
187
|
+
if (!(column in this.#newRowState.errors)) return;
|
|
148
188
|
const errors = {
|
|
149
189
|
...this.#newRowState.errors
|
|
150
190
|
};
|
|
151
191
|
delete errors[column];
|
|
152
192
|
this.#setNewRowState({
|
|
153
193
|
...this.#newRowState,
|
|
154
|
-
errors
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
194
|
+
errors
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
clearNewRow() {
|
|
198
|
+
this.#setNewRowState({
|
|
199
|
+
columns: this.#newRowState.columns,
|
|
200
|
+
draftRevision: this.#newRowState.draftRevision + 1,
|
|
201
|
+
errors: {},
|
|
202
|
+
requiredColumns: this.#newRowState.requiredColumns,
|
|
203
|
+
submitting: false,
|
|
204
|
+
values: {}
|
|
159
205
|
});
|
|
160
206
|
}
|
|
161
207
|
async addNewRow() {
|
|
@@ -167,8 +213,13 @@ class EditSession extends EventEmitter {
|
|
|
167
213
|
column,
|
|
168
214
|
"Value required"
|
|
169
215
|
]));
|
|
170
|
-
const
|
|
171
|
-
|
|
216
|
+
const { rowErrorColumn, ...newRowState } = this.#newRowState;
|
|
217
|
+
const { [rowErrorColumn ?? ""]: _serverError, ...fieldErrors } = newRowState.errors;
|
|
218
|
+
const errors = void 0 === rowErrorColumn ? {
|
|
219
|
+
...newRowState.errors,
|
|
220
|
+
...missingErrors
|
|
221
|
+
} : {
|
|
222
|
+
...fieldErrors,
|
|
172
223
|
...missingErrors
|
|
173
224
|
};
|
|
174
225
|
if (Object.keys(errors).length > 0) {
|
|
@@ -186,7 +237,8 @@ class EditSession extends EventEmitter {
|
|
|
186
237
|
type: "SUCCESS_RESULT"
|
|
187
238
|
};
|
|
188
239
|
this.#setNewRowState({
|
|
189
|
-
...
|
|
240
|
+
...newRowState,
|
|
241
|
+
errors,
|
|
190
242
|
submitting: true
|
|
191
243
|
});
|
|
192
244
|
try {
|
|
@@ -200,6 +252,7 @@ class EditSession extends EventEmitter {
|
|
|
200
252
|
errors: finalColumn ? {
|
|
201
253
|
[finalColumn]: response.errorMessage
|
|
202
254
|
} : this.#newRowState.errors,
|
|
255
|
+
rowErrorColumn: finalColumn,
|
|
203
256
|
submitting: false
|
|
204
257
|
});
|
|
205
258
|
return response;
|
|
@@ -221,6 +274,7 @@ class EditSession extends EventEmitter {
|
|
|
221
274
|
errors: finalColumn ? {
|
|
222
275
|
[finalColumn]: errorMessage
|
|
223
276
|
} : this.#newRowState.errors,
|
|
277
|
+
rowErrorColumn: finalColumn,
|
|
224
278
|
submitting: false
|
|
225
279
|
});
|
|
226
280
|
return {
|
|
@@ -282,6 +336,14 @@ class EditSession extends EventEmitter {
|
|
|
282
336
|
hasRowChanges(key) {
|
|
283
337
|
return this.#rowEdits.has(key);
|
|
284
338
|
}
|
|
339
|
+
getEditedValues(key) {
|
|
340
|
+
const cellEdits = this.#rowEdits.get(key)?.cellEdits;
|
|
341
|
+
if (!cellEdits) return {};
|
|
342
|
+
return Object.fromEntries(Array.from(cellEdits, ([column, { editedValue }])=>[
|
|
343
|
+
column,
|
|
344
|
+
editedValue
|
|
345
|
+
]));
|
|
346
|
+
}
|
|
285
347
|
isCellEdited(key, columnName) {
|
|
286
348
|
const cellEdit = this.#rowEdits.get(key)?.cellEdits.get(columnName);
|
|
287
349
|
return cellEdit?.isValid === true && cellEdit.originalValue !== cellEdit.editedValue;
|
|
@@ -364,6 +426,9 @@ class EditSession extends EventEmitter {
|
|
|
364
426
|
this.#lifecycle = lifecycle;
|
|
365
427
|
this.emit("lifecycle", lifecycle);
|
|
366
428
|
}
|
|
429
|
+
#handleSessionUnsubscribed = ()=>{
|
|
430
|
+
if (this.canCancel) this.end(false);
|
|
431
|
+
};
|
|
367
432
|
#enqueue(operation) {
|
|
368
433
|
const result = this.#transitionQueue.then(operation);
|
|
369
434
|
this.#transitionQueue = result.then(()=>void 0, ()=>void 0);
|
|
@@ -388,6 +453,7 @@ class EditSession extends EventEmitter {
|
|
|
388
453
|
const sessionDataSource = "beginEditSession" === this.#editSessionApi ? await sourceDataSource?.beginEditSession?.(toEditSessionMode(copyOption)) : await sourceDataSource?.createSessionDataSource?.(copyOption, sessionType);
|
|
389
454
|
if (!sessionDataSource) throw new Error(`[EditSession] datasource does not support ${this.#editSessionApi}`);
|
|
390
455
|
this.#sessionDataSource = sessionDataSource;
|
|
456
|
+
sessionDataSource.on?.("unsubscribed", this.#handleSessionUnsubscribed);
|
|
391
457
|
this.#setStale(false);
|
|
392
458
|
this.#setLifecycle({
|
|
393
459
|
status: "active",
|
|
@@ -424,6 +490,9 @@ class EditSession extends EventEmitter {
|
|
|
424
490
|
get dataSource() {
|
|
425
491
|
return this.#sessionDataSource ?? this.#sourceTableDataSource;
|
|
426
492
|
}
|
|
493
|
+
get sourceDataSource() {
|
|
494
|
+
return this.#sourceTableDataSource;
|
|
495
|
+
}
|
|
427
496
|
get sessionDataSource() {
|
|
428
497
|
return this.#sessionDataSource;
|
|
429
498
|
}
|
|
@@ -446,6 +515,7 @@ class EditSession extends EventEmitter {
|
|
|
446
515
|
const response = await this.dataSource?.endEditSession?.(saveChanges, force);
|
|
447
516
|
if (isRpcError(response)) throw new Error(response.errorMessage);
|
|
448
517
|
}
|
|
518
|
+
sessionDataSource.removeListener?.("unsubscribed", this.#handleSessionUnsubscribed);
|
|
449
519
|
this.#clearEdits();
|
|
450
520
|
this.#sessionDataSource = void 0;
|
|
451
521
|
this.#setLifecycle({
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Checkbox } from "@salt-ds/core";
|
|
3
|
+
import { useCallback, useEffect, useState } from "react";
|
|
4
|
+
const isChecked = (value)=>true === value || "true" === value;
|
|
5
|
+
const EditControlCheckbox = ({ className, onCommit, value })=>{
|
|
6
|
+
const [checked, setChecked] = useState(isChecked(value));
|
|
7
|
+
useEffect(()=>{
|
|
8
|
+
setChecked(isChecked(value));
|
|
9
|
+
}, [
|
|
10
|
+
value
|
|
11
|
+
]);
|
|
12
|
+
const handleChange = useCallback((evt)=>{
|
|
13
|
+
setChecked(evt.target.checked);
|
|
14
|
+
onCommit(evt, evt.target.checked);
|
|
15
|
+
}, [
|
|
16
|
+
onCommit
|
|
17
|
+
]);
|
|
18
|
+
return /*#__PURE__*/ jsx(Checkbox, {
|
|
19
|
+
checked: checked,
|
|
20
|
+
className: className,
|
|
21
|
+
"data-edit-control": true,
|
|
22
|
+
onChange: handleChange
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
export { EditControlCheckbox };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Dropdown, Option } from "@salt-ds/core";
|
|
3
|
+
import { useCallback } from "react";
|
|
4
|
+
const toOption = (value)=>"string" == typeof value ? {
|
|
5
|
+
label: value,
|
|
6
|
+
value
|
|
7
|
+
} : value;
|
|
8
|
+
const EditControlDropdown = ({ className, onCommit, value, values })=>{
|
|
9
|
+
const options = values.map(toOption);
|
|
10
|
+
const selectedValue = void 0 === value ? "" : String(value);
|
|
11
|
+
const selected = options.find((option)=>option.value === selectedValue);
|
|
12
|
+
const handleSelectionChange = useCallback((evt, [option])=>{
|
|
13
|
+
if (option) onCommit(evt, option.value);
|
|
14
|
+
}, [
|
|
15
|
+
onCommit
|
|
16
|
+
]);
|
|
17
|
+
return /*#__PURE__*/ jsx(Dropdown, {
|
|
18
|
+
bordered: true,
|
|
19
|
+
className: className,
|
|
20
|
+
"data-edit-control": true,
|
|
21
|
+
"data-icon": "triangle-down",
|
|
22
|
+
onSelectionChange: handleSelectionChange,
|
|
23
|
+
placeholder: "Please select value",
|
|
24
|
+
selected: selected ? [
|
|
25
|
+
selected
|
|
26
|
+
] : [],
|
|
27
|
+
value: selected?.label ?? selectedValue,
|
|
28
|
+
children: options.map((option)=>/*#__PURE__*/ jsx(Option, {
|
|
29
|
+
value: option,
|
|
30
|
+
children: option.label
|
|
31
|
+
}, option.value))
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
export { EditControlDropdown };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { useLookupValues } from "../lookup-values/useLookupValues.js";
|
|
4
|
+
import { EditControlDropdown } from "./EditControlDropdown.js";
|
|
5
|
+
const EditControlLookupDropdown = ({ className, lookup: { labelColumn, table, valueColumn }, onCommit, value })=>{
|
|
6
|
+
const optionMap = useMemo(()=>({
|
|
7
|
+
label: labelColumn,
|
|
8
|
+
value: valueColumn
|
|
9
|
+
}), [
|
|
10
|
+
labelColumn,
|
|
11
|
+
valueColumn
|
|
12
|
+
]);
|
|
13
|
+
const lookupValues = useLookupValues({
|
|
14
|
+
enabled: true,
|
|
15
|
+
optionMap,
|
|
16
|
+
table
|
|
17
|
+
});
|
|
18
|
+
const values = useMemo(()=>lookupValues.map(({ label, value })=>({
|
|
19
|
+
label,
|
|
20
|
+
value: String(value)
|
|
21
|
+
})), [
|
|
22
|
+
lookupValues
|
|
23
|
+
]);
|
|
24
|
+
return /*#__PURE__*/ jsx(EditControlDropdown, {
|
|
25
|
+
className: className,
|
|
26
|
+
onCommit: onCommit,
|
|
27
|
+
value: value,
|
|
28
|
+
values: values
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
export { EditControlLookupDropdown };
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { VuuDatePicker, VuuInput, VuuTimePicker, VuuTypeaheadInput } from "@vuu-ui/vuu-ui-controls";
|
|
3
|
+
import { EpochTimestamp, getTemporalInfo, isDataValueEditable, isTimeDataValueWithMilliseconds, isValidTimeString, isValidTimeStringMillis, parseTemporalInput } from "@vuu-ui/vuu-utils";
|
|
4
|
+
import { EditControlCheckbox } from "./EditControlCheckbox.js";
|
|
5
|
+
import { EditControlDropdown } from "./EditControlDropdown.js";
|
|
6
|
+
import { EditControlLookupDropdown } from "./EditControlLookupDropdown.js";
|
|
7
|
+
const VARIANT_MIN_CHARACTER_COUNT = {
|
|
8
|
+
default: void 0,
|
|
9
|
+
pick: 0,
|
|
10
|
+
search: 1
|
|
11
|
+
};
|
|
12
|
+
const isTypeaheadEditor = (editor)=>editor?.name === "typeahead";
|
|
13
|
+
const isValueListEditor = (editor)=>Array.isArray(editor?.values);
|
|
14
|
+
const isLookupEditor = (editor)=>editor?.lookup !== void 0;
|
|
15
|
+
const isCheckboxEditor = (editor)=>editor?.name === "checkbox-cell" || editor?.name === "toggle-cell";
|
|
16
|
+
const timeOfDayPattern = /^\d{2}:\d{2}(:\d{2}(\.\d{1,9})?)?$/;
|
|
17
|
+
const toEpochTimestamp = (value, { encoding })=>{
|
|
18
|
+
if (("string" == typeof value || "number" == typeof value) && !timeOfDayPattern.test(`${value}`)) return EpochTimestamp.fromWire(value, encoding);
|
|
19
|
+
};
|
|
20
|
+
const pad = (n)=>`${n}`.padStart(2, "0");
|
|
21
|
+
function toTimeString(value, timestamp, { timeZone }, milliseconds = false) {
|
|
22
|
+
if (timestamp) {
|
|
23
|
+
const { hour, minute, second, millisecond } = timestamp.getFields(timeZone);
|
|
24
|
+
const timeString = `${pad(hour)}:${pad(minute)}:${pad(second)}`;
|
|
25
|
+
return milliseconds ? `${timeString}.${`${millisecond}`.padStart(3, "0")}` : timeString;
|
|
26
|
+
}
|
|
27
|
+
if ("string" == typeof value) if (milliseconds) {
|
|
28
|
+
const timeString = 8 === value.length ? `${value}.000` : value.slice(0, 12);
|
|
29
|
+
if (isValidTimeStringMillis(timeString)) return timeString;
|
|
30
|
+
} else {
|
|
31
|
+
const timeString = value.slice(0, 8);
|
|
32
|
+
if (isValidTimeString(timeString)) return timeString;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const getDataItemEditControl = ({ InputProps, TypeaheadProps, className, commitOnBlur, commitWhenCleared, dataDescriptor, editOperation = "update", errorMessage, onCommit, purpose = "edit", table, variant })=>{
|
|
36
|
+
const { editor } = dataDescriptor;
|
|
37
|
+
const temporalInfo = getTemporalInfo(dataDescriptor);
|
|
38
|
+
const value = InputProps?.value ?? InputProps?.inputProps?.value;
|
|
39
|
+
if (!isDataValueEditable(dataDescriptor, editOperation, true)) return /*#__PURE__*/ jsx(VuuInput, {
|
|
40
|
+
variant: "secondary",
|
|
41
|
+
...InputProps,
|
|
42
|
+
onCommit: onCommit,
|
|
43
|
+
readOnly: true,
|
|
44
|
+
"data-edit-control": true
|
|
45
|
+
});
|
|
46
|
+
if (temporalInfo?.kind === "time") {
|
|
47
|
+
const { value, onChange } = InputProps?.inputProps ?? {};
|
|
48
|
+
const baseValue = toEpochTimestamp(value, temporalInfo);
|
|
49
|
+
const handleCommitTime = (evt, timeString)=>{
|
|
50
|
+
if (baseValue) {
|
|
51
|
+
const timestamp = parseTemporalInput(timeString, temporalInfo, baseValue);
|
|
52
|
+
onCommit(evt, timestamp ? `${timestamp.toWire(temporalInfo.encoding)}` : "");
|
|
53
|
+
} else onCommit(evt, timeString);
|
|
54
|
+
};
|
|
55
|
+
return isTimeDataValueWithMilliseconds(dataDescriptor) ? /*#__PURE__*/ jsx(VuuTimePicker, {
|
|
56
|
+
className: className,
|
|
57
|
+
milliseconds: true,
|
|
58
|
+
value: "" === value ? "" : toTimeString(value, baseValue, temporalInfo, true),
|
|
59
|
+
onChange: onChange,
|
|
60
|
+
onCommit: handleCommitTime,
|
|
61
|
+
"data-edit-control": true
|
|
62
|
+
}) : /*#__PURE__*/ jsx(VuuTimePicker, {
|
|
63
|
+
className: className,
|
|
64
|
+
value: "" === value ? "" : toTimeString(value, baseValue, temporalInfo),
|
|
65
|
+
onChange: onChange,
|
|
66
|
+
onCommit: handleCommitTime,
|
|
67
|
+
"data-edit-control": true
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
if (temporalInfo) {
|
|
71
|
+
const baseValue = toEpochTimestamp(InputProps?.inputProps?.value, temporalInfo);
|
|
72
|
+
const handleCommitDate = (evt, startOfDayMillis)=>{
|
|
73
|
+
let timestamp = EpochTimestamp.fromMillis(startOfDayMillis);
|
|
74
|
+
if (baseValue && "datetime" === temporalInfo.kind) {
|
|
75
|
+
const { year, month, day } = timestamp.getFields(temporalInfo.timeZone);
|
|
76
|
+
const { hour, minute, second, millisecond } = baseValue.getFields(temporalInfo.timeZone);
|
|
77
|
+
timestamp = EpochTimestamp.fromFields({
|
|
78
|
+
year,
|
|
79
|
+
month,
|
|
80
|
+
day,
|
|
81
|
+
hour,
|
|
82
|
+
minute,
|
|
83
|
+
second,
|
|
84
|
+
millisecond
|
|
85
|
+
}, temporalInfo.timeZone, baseValue.subMilliNanos);
|
|
86
|
+
}
|
|
87
|
+
onCommit(evt, `${timestamp.toWire(temporalInfo.encoding)}`);
|
|
88
|
+
};
|
|
89
|
+
return /*#__PURE__*/ jsx(VuuDatePicker, {
|
|
90
|
+
className: className,
|
|
91
|
+
onCommit: handleCommitDate,
|
|
92
|
+
timeZone: temporalInfo.timeZone,
|
|
93
|
+
value: baseValue,
|
|
94
|
+
"data-edit-control": true
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
if ("edit" === purpose && (isCheckboxEditor(editor) || void 0 === editor && "boolean" === dataDescriptor.serverDataType)) return /*#__PURE__*/ jsx(EditControlCheckbox, {
|
|
98
|
+
className: className,
|
|
99
|
+
onCommit: onCommit,
|
|
100
|
+
value: value
|
|
101
|
+
});
|
|
102
|
+
if (isLookupEditor(editor)) return /*#__PURE__*/ jsx(EditControlLookupDropdown, {
|
|
103
|
+
className: className,
|
|
104
|
+
lookup: editor.lookup,
|
|
105
|
+
onCommit: onCommit,
|
|
106
|
+
value: value
|
|
107
|
+
});
|
|
108
|
+
else if (isValueListEditor(editor)) return /*#__PURE__*/ jsx(EditControlDropdown, {
|
|
109
|
+
className: className,
|
|
110
|
+
onCommit: onCommit,
|
|
111
|
+
value: value,
|
|
112
|
+
values: editor.values
|
|
113
|
+
});
|
|
114
|
+
else if (table && (isTypeaheadEditor(editor) || "filter" === purpose && "string" === dataDescriptor.serverDataType)) {
|
|
115
|
+
const minCharacterCountToTriggerSuggestions = TypeaheadProps?.minCharacterCountToTriggerSuggestions ?? (isTypeaheadEditor(editor) ? editor.minCharacterCountToTriggerSuggestions : void 0) ?? VARIANT_MIN_CHARACTER_COUNT[variant ?? "default"];
|
|
116
|
+
return /*#__PURE__*/ jsx(VuuTypeaheadInput, {
|
|
117
|
+
...InputProps,
|
|
118
|
+
...TypeaheadProps,
|
|
119
|
+
className: className,
|
|
120
|
+
column: dataDescriptor.name,
|
|
121
|
+
"data-edit-control": true,
|
|
122
|
+
"data-variant": variant,
|
|
123
|
+
minCharacterCountToTriggerSuggestions: minCharacterCountToTriggerSuggestions,
|
|
124
|
+
onCommit: onCommit,
|
|
125
|
+
table: table
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return /*#__PURE__*/ jsx(VuuInput, {
|
|
129
|
+
variant: "secondary",
|
|
130
|
+
...InputProps,
|
|
131
|
+
className: className,
|
|
132
|
+
commitOnBlur: commitOnBlur,
|
|
133
|
+
commitWhenCleared: commitWhenCleared,
|
|
134
|
+
onCommit: onCommit,
|
|
135
|
+
errorMessage: errorMessage,
|
|
136
|
+
"data-edit-control": true
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
export { getDataItemEditControl };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
const toError = (cause)=>cause instanceof Error ? cause : new Error(String(cause));
|
|
2
|
+
const reportEditError = (source, onError, cause, operation)=>{
|
|
3
|
+
const error = toError(cause);
|
|
4
|
+
if (onError) onError(error, operation);
|
|
5
|
+
else console.error("[%s] %s edit session failed", source, operation, error);
|
|
6
|
+
};
|
|
7
|
+
export { reportEditError, toError };
|