@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.
Files changed (57) hide show
  1. package/README.md +40 -3
  2. package/package.json +7 -6
  3. package/src/DirectEditSession.js +8 -1
  4. package/src/EditButtons.js +6 -5
  5. package/src/EditModeProvider.js +14 -5
  6. package/src/EditSession.js +78 -8
  7. package/src/edit-control/EditControlCheckbox.js +25 -0
  8. package/src/edit-control/EditControlDropdown.js +34 -0
  9. package/src/edit-control/EditControlLookupDropdown.js +31 -0
  10. package/src/edit-control/get-data-item-edit-control.js +139 -0
  11. package/src/edit-control/index.js +4 -0
  12. package/src/edit-errors.js +7 -0
  13. package/src/edit-field/EditField.js +117 -24
  14. package/src/edit-field/useEditField.js +66 -52
  15. package/src/edit-form/CreateRowForm.js +37 -0
  16. package/src/edit-form/EditForm.css.js +32 -0
  17. package/src/edit-form/EditForm.js +85 -0
  18. package/src/edit-form/EditFormButtons.js +48 -0
  19. package/src/edit-form/EditFormContext.js +4 -0
  20. package/src/edit-form/index.js +4 -0
  21. package/src/form-fields.js +40 -0
  22. package/src/index.js +9 -0
  23. package/src/lookup-values/useLookupOptions.js +106 -0
  24. package/src/useConfirmDiscard.js +34 -0
  25. package/src/useCustomEditField.js +76 -0
  26. package/src/useEditForm.js +306 -0
  27. package/src/useEditSessionState.js +72 -0
  28. package/src/useEditable.js +26 -9
  29. package/src/useEditableColumns.js +105 -0
  30. package/src/useEditableTable.js +32 -9
  31. package/types/DirectEditSession.d.ts +6 -1
  32. package/types/EditButtons.d.ts +20 -3
  33. package/types/EditModeProvider.d.ts +10 -2
  34. package/types/EditSession.d.ts +18 -1
  35. package/types/edit-control/EditControlCheckbox.d.ts +8 -0
  36. package/types/edit-control/EditControlDropdown.d.ts +14 -0
  37. package/types/edit-control/EditControlLookupDropdown.d.ts +13 -0
  38. package/types/edit-control/get-data-item-edit-control.d.ts +40 -0
  39. package/types/edit-control/index.d.ts +4 -0
  40. package/types/edit-errors.d.ts +9 -0
  41. package/types/edit-field/EditField.d.ts +37 -3
  42. package/types/edit-field/useEditField.d.ts +2 -1
  43. package/types/edit-form/CreateRowForm.d.ts +29 -0
  44. package/types/edit-form/EditForm.d.ts +36 -0
  45. package/types/edit-form/EditFormButtons.d.ts +18 -0
  46. package/types/edit-form/EditFormContext.d.ts +59 -0
  47. package/types/edit-form/index.d.ts +4 -0
  48. package/types/form-fields.d.ts +32 -0
  49. package/types/index.d.ts +12 -2
  50. package/types/lookup-values/useLookupOptions.d.ts +35 -0
  51. package/types/useConfirmDiscard.d.ts +23 -0
  52. package/types/useCustomEditField.d.ts +51 -0
  53. package/types/useEditForm.d.ts +127 -0
  54. package/types/useEditSessionState.d.ts +27 -0
  55. package/types/useEditable.d.ts +30 -3
  56. package/types/useEditableColumns.d.ts +79 -0
  57. 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 delegates to `addRow()`.
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",
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/vuu-ui-controls": "5.0.0-alpha.2",
16
- "@vuu-ui/vuu-utils": "5.0.0-alpha.2"
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.2",
20
- "@vuu-ui/vuu-protocol-types": "5.0.0-alpha.2",
21
- "@vuu-ui/vuu-table-types": "5.0.0-alpha.2"
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"
@@ -1,9 +1,11 @@
1
1
  import { EventEmitter } from "@vuu-ui/vuu-utils";
2
2
  class DirectEditSession extends EventEmitter {
3
3
  #dataSource;
4
- constructor({ dataSource }){
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);
@@ -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 { useEditState } from "./useEditState.js";
5
- const EditButtons = ({ canCancel, canSave: _canSave, confirmCancel, confirmSave, editSession, hasSelection = false, onCancel, onDelete, onSave, saveLabel = "Save" })=>{
6
- const editState = useEditState(editSession);
7
- const isDirty = editSession?.isDirty ?? false;
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: !editSession?.canSave,
40
+ disabled: !canSave,
40
41
  onClick: handleSave,
41
42
  sentiment: "accented",
42
43
  children: "stale" === editState ? `${saveLabel} (force)` : saveLabel
@@ -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: ()=>"EditModeProvider in place"
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
- return /*#__PURE__*/ jsx(EditModeContext.Provider, {
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
  };
@@ -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
- values: {
156
- ...this.#newRowState.values,
157
- [column]: value
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 errors = {
171
- ...this.#newRowState.errors,
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
- ...this.#newRowState,
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,4 @@
1
+ export * from "./EditControlCheckbox.js";
2
+ export * from "./EditControlDropdown.js";
3
+ export * from "./EditControlLookupDropdown.js";
4
+ export * from "./get-data-item-edit-control.js";
@@ -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 };