@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
@@ -0,0 +1,105 @@
1
+ import { isDataValueEditable } from "@vuu-ui/vuu-utils";
2
+ import { useMemo } from "react";
3
+ import { UNDO_CELL_RENDERER } from "./UndoCellRenderer.js";
4
+ const DEFAULT_READ_ONLY_COLUMNS = [
5
+ "vuuCreatedTimestamp",
6
+ "vuuUpdatedTimestamp",
7
+ "vuuMsg"
8
+ ];
9
+ const UNDO_COLUMN = {
10
+ name: "undo",
11
+ source: "client",
12
+ width: 80,
13
+ type: {
14
+ name: "string",
15
+ renderer: {
16
+ name: UNDO_CELL_RENDERER
17
+ }
18
+ }
19
+ };
20
+ const typeName = (column)=>{
21
+ if ("string" == typeof column.type) return column.type;
22
+ if (column.type) return column.type.name;
23
+ return "string";
24
+ };
25
+ const isReadOnlyColumn = (name)=>DEFAULT_READ_ONLY_COLUMNS.includes(name);
26
+ const narrowEditable = (schemaEditable = true, columnEditable = true)=>{
27
+ if (true === schemaEditable) return columnEditable;
28
+ if (false === schemaEditable || false === columnEditable) return false;
29
+ const insert = isDataValueEditable({
30
+ editable: schemaEditable
31
+ }, "insert") && isDataValueEditable({
32
+ editable: columnEditable
33
+ }, "insert");
34
+ const update = isDataValueEditable({
35
+ editable: schemaEditable
36
+ }, "update") && isDataValueEditable({
37
+ editable: columnEditable
38
+ }, "update");
39
+ return insert === update ? insert : {
40
+ insert,
41
+ update
42
+ };
43
+ };
44
+ const withEditor = (column, editor)=>{
45
+ if (void 0 === editor || "typeahead" === editor.name) return column;
46
+ return {
47
+ ...column,
48
+ type: {
49
+ name: typeName(column),
50
+ renderer: editor
51
+ }
52
+ };
53
+ };
54
+ const toViewColumn = (column)=>void 0 === column.editable || false === column.editable ? column : {
55
+ ...column,
56
+ editable: false
57
+ };
58
+ const getEditableColumns = ({ actionColumn = false, columns, columnsDiverge = false, editSchema, isEditing, schema = editSchema, undoColumn = false })=>{
59
+ if (!isEditing) return columns.some((c)=>void 0 !== c.editable && false !== c.editable) ? columns.map(toViewColumn) : columns;
60
+ const baseColumns = columnsDiverge && editSchema ? editSchema.columns.filter(({ name })=>"vuuAction" !== name).map((schemaColumn)=>columns.find(({ name })=>name === schemaColumn.name) ?? {
61
+ name: schemaColumn.name,
62
+ serverDataType: schemaColumn.serverDataType
63
+ }) : columns;
64
+ const editColumns = baseColumns.map((column)=>{
65
+ if ("client" === column.source) return column;
66
+ if (isReadOnlyColumn(column.name)) return void 0 === column.editable ? column : {
67
+ ...column,
68
+ editable: false
69
+ };
70
+ const schemaColumn = schema?.columns.find(({ name })=>name === column.name);
71
+ const editable = narrowEditable(schemaColumn?.editable, column.editable);
72
+ return withEditor({
73
+ ...column,
74
+ editable
75
+ }, column.editor);
76
+ });
77
+ if (actionColumn) editColumns.push({
78
+ hidden: "hidden" === actionColumn,
79
+ name: "vuuAction",
80
+ serverDataType: "string"
81
+ });
82
+ if (undoColumn) editColumns.push(true === undoColumn ? UNDO_COLUMN : {
83
+ ...UNDO_COLUMN,
84
+ ...undoColumn
85
+ });
86
+ return editColumns;
87
+ };
88
+ const useEditableColumns = ({ actionColumn, columns, columnsDiverge, editSchema, isEditing, schema, undoColumn })=>useMemo(()=>getEditableColumns({
89
+ actionColumn,
90
+ columns,
91
+ columnsDiverge,
92
+ editSchema,
93
+ isEditing,
94
+ schema,
95
+ undoColumn
96
+ }), [
97
+ actionColumn,
98
+ columns,
99
+ columnsDiverge,
100
+ editSchema,
101
+ isEditing,
102
+ schema,
103
+ undoColumn
104
+ ]);
105
+ export { DEFAULT_READ_ONLY_COLUMNS, UNDO_COLUMN, getEditableColumns, narrowEditable, useEditableColumns };
@@ -3,14 +3,20 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
3
  import { EditSession } from "./EditSession.js";
4
4
  import { EDIT_ACTION_ROW_CLASS_NAME_GENERATOR } from "./editActionRowClassNameGenerator.js";
5
5
  import { isEditRowReadOnly } from "./edit-utils.js";
6
+ import { useEditMode } from "./EditModeProvider.js";
7
+ import { reportEditError } from "./edit-errors.js";
6
8
  const EDIT_ACTION_ROW_CLASS_NAME_GENERATORS = [
7
9
  EDIT_ACTION_ROW_CLASS_NAME_GENERATOR
8
10
  ];
9
- const useEditableTable = ({ columns, dataSource: dataSourceProp, deleteMode = "soft", editSessionApi = "createSessionDataSource", copyOption = "All", isEditMode, onCancel, onSave, rowDefaults, table })=>{
11
+ const useEditableTable = ({ columns, dataSource: dataSourceProp, deleteMode = "soft", editSessionApi = "createSessionDataSource", copyOption = "All", isEditMode: isEditModeProp, onCancel, onError, onSave, rowDefaults, table })=>{
10
12
  const { VuuDataSource } = useData();
13
+ const { isEditMode: isEditModeContext } = useEditMode();
14
+ const isEditMode = isEditModeProp ?? isEditModeContext;
11
15
  const [selectionCount, setSelectionCount] = useState(0);
16
+ const onErrorRef = useRef(onError);
17
+ onErrorRef.current = onError;
12
18
  useLayoutEffectSkipFirst(()=>{
13
- console.warn("[useEditableTable] columns and or table changed");
19
+ if ("production" !== process.env.NODE_ENV && !dataSourceProp) console.warn("[useEditableTable] columns or table changed, a new DataSource and EditSession will be created. Pass stable (memoized) values.");
14
20
  }, [
15
21
  columns,
16
22
  table
@@ -52,7 +58,7 @@ const useEditableTable = ({ columns, dataSource: dataSourceProp, deleteMode = "s
52
58
  setSelectionCount(0);
53
59
  onCancel();
54
60
  } catch (error) {
55
- console.error("[useEditableTable] cancel edit session failed", error);
61
+ reportEditError("useEditableTable", onErrorRef.current, error, "cancel");
56
62
  }
57
63
  }, [
58
64
  editSession,
@@ -64,14 +70,19 @@ const useEditableTable = ({ columns, dataSource: dataSourceProp, deleteMode = "s
64
70
  setSelectionCount(0);
65
71
  onSave();
66
72
  } catch (error) {
67
- console.error("[useEditableTable] save edit session failed", error);
73
+ reportEditError("useEditableTable", onErrorRef.current, error, "save");
68
74
  }
69
75
  }, [
70
76
  editSession,
71
77
  onSave
72
78
  ]);
73
79
  const handleDelete = useCallback(async ()=>{
74
- await editSession.deleteSelectedRows();
80
+ try {
81
+ const response = await editSession.deleteSelectedRows();
82
+ if ("ERROR_RESULT" === response.type) reportEditError("useEditableTable", onErrorRef.current, new Error(response.errorMessage), "delete");
83
+ } catch (error) {
84
+ reportEditError("useEditableTable", onErrorRef.current, error, "delete");
85
+ }
75
86
  }, [
76
87
  editSession
77
88
  ]);
@@ -90,10 +101,20 @@ const useEditableTable = ({ columns, dataSource: dataSourceProp, deleteMode = "s
90
101
  editSession.reconcileWithSessionSchema();
91
102
  setSubscribedSessionDataSource(sessionDataSource);
92
103
  };
104
+ const handleUnsubscribed = ()=>{
105
+ if (editSession.canCancel) editSession.end().then(()=>{
106
+ setSelectionCount(0);
107
+ onCancelRef.current();
108
+ });
109
+ };
93
110
  setSubscribedSessionDataSource(void 0);
94
111
  sessionDataSource.on("subscribed", handleSubscribed);
112
+ sessionDataSource.on("unsubscribed", handleUnsubscribed);
95
113
  if ("subscribed" === sessionDataSource.status && sessionDataSource.tableSchema) handleSubscribed();
96
- return ()=>sessionDataSource.removeListener("subscribed", handleSubscribed);
114
+ return ()=>{
115
+ sessionDataSource.removeListener("subscribed", handleSubscribed);
116
+ sessionDataSource.removeListener("unsubscribed", handleUnsubscribed);
117
+ };
97
118
  }, [
98
119
  editSession,
99
120
  sessionDataSource
@@ -120,9 +141,9 @@ const useEditableTable = ({ columns, dataSource: dataSourceProp, deleteMode = "s
120
141
  const transition = isEditMode ? editSession.begin(copyOption) : editSession.end();
121
142
  transition.catch((error)=>{
122
143
  if (isEditMode) {
123
- console.error("[useEditableTable] begin edit session failed", error);
144
+ reportEditError("useEditableTable", onErrorRef.current, error, "begin");
124
145
  onCancelRef.current();
125
- } else console.error("[useEditableTable] end edit session failed", error);
146
+ } else reportEditError("useEditableTable", onErrorRef.current, error, "cancel");
126
147
  });
127
148
  }, [
128
149
  copyOption,
@@ -130,7 +151,7 @@ const useEditableTable = ({ columns, dataSource: dataSourceProp, deleteMode = "s
130
151
  isEditMode
131
152
  ]);
132
153
  const canCancel = "active" === lifecycle.status || "error" === lifecycle.status && "end" === lifecycle.operation;
133
- const canSave = canCancel && ("dirty" === editState || "stale" === editState) && 0 === editSession.invalidCount;
154
+ const canSave = canCancel && ("dirty" === editState || "stale" === editState);
134
155
  const isEditSessionReady = isEditMode && void 0 !== sessionDataSource && sessionDataSource === editSession.sessionDataSource && subscribedSessionDataSource === sessionDataSource && "subscribed" === sessionDataSource.status && void 0 !== sessionDataSource.tableSchema;
135
156
  const editSchema = subscribedSessionDataSource?.tableSchema;
136
157
  const viewSchema = sourceDataSource.tableSchema;
@@ -142,6 +163,8 @@ const useEditableTable = ({ columns, dataSource: dataSourceProp, deleteMode = "s
142
163
  dataSource,
143
164
  editSchema,
144
165
  editSession,
166
+ editState,
167
+ isEditMode,
145
168
  lifecycle,
146
169
  hasSelection: selectionCount > 0,
147
170
  isEditSessionReady,
@@ -2,8 +2,13 @@ import type { DataSource } from "@vuu-ui/vuu-data-types";
2
2
  import type { RpcResult, VuuRowDataItemType } from "@vuu-ui/vuu-protocol-types";
3
3
  import { EventEmitter } from "@vuu-ui/vuu-utils";
4
4
  import type { TableEditSession, TableEditSessionEvents } from "./TableEditSession";
5
+ /**
6
+ * Checks a value before it is sent. Return an error message to reject it.
7
+ */
8
+ export type DirectEditValidator = (key: string, columnName: string, value: VuuRowDataItemType) => string | undefined;
5
9
  export type DirectEditSessionConstructorProps = {
6
10
  dataSource: DataSource;
11
+ validate?: DirectEditValidator;
7
12
  };
8
13
  /**
9
14
  * A lightweight edit session for services that allow cells to be edited
@@ -13,7 +18,7 @@ export type DirectEditSessionConstructorProps = {
13
18
  */
14
19
  export declare class DirectEditSession extends EventEmitter<TableEditSessionEvents> implements TableEditSession {
15
20
  #private;
16
- constructor({ dataSource }: DirectEditSessionConstructorProps);
21
+ constructor({ dataSource, validate }: DirectEditSessionConstructorProps);
17
22
  get dataSource(): DataSource;
18
23
  get inEditMode(): boolean;
19
24
  isCellEdited(_key: string, _columnName: string): boolean;
@@ -1,14 +1,31 @@
1
1
  import type { EditSession } from "./EditSession";
2
2
  export interface EditButtonProps {
3
- canCancel: boolean;
4
- canSave: boolean;
3
+ /**
4
+ * Enables the Cancel button. Defaults to `editSession.canCancel`, so only
5
+ * provide this to override the session-derived value.
6
+ */
7
+ canCancel?: boolean;
8
+ /**
9
+ * An extra condition for enabling Save. Save is only enabled when this is
10
+ * not false and `editSession.canSave` is true.
11
+ */
12
+ canSave?: boolean;
5
13
  editSession?: EditSession;
6
14
  hasSelection?: boolean;
7
15
  onCancel?: () => void;
8
16
  onDelete?: () => void;
9
17
  onSave: (force?: boolean) => void;
10
18
  saveLabel?: string;
19
+ /** Return false (or resolve false) to abort the save. */
11
20
  confirmSave?: () => boolean | Promise<boolean>;
21
+ /**
22
+ * Called before cancelling a session with unsaved changes. Return false
23
+ * (or resolve false) to keep editing. See `useConfirmDiscard`.
24
+ */
12
25
  confirmCancel?: () => boolean | Promise<boolean>;
13
26
  }
14
- export declare const EditButtons: ({ canCancel, canSave: _canSave, confirmCancel, confirmSave, editSession, hasSelection, onCancel, onDelete, onSave, saveLabel, }: EditButtonProps) => import("react").JSX.Element;
27
+ /**
28
+ * Delete, Save and Cancel buttons for an edit session. Save switches to
29
+ * "Save (force)" when the last save was rejected as stale.
30
+ */
31
+ export declare const EditButtons: ({ canCancel: canCancelProp, canSave: canSaveProp, confirmCancel, confirmSave, editSession, hasSelection, onCancel, onDelete, onSave, saveLabel, }: EditButtonProps) => import("react").JSX.Element;
@@ -4,11 +4,19 @@ export interface EditModeContextProps {
4
4
  setEditMode: (inEditMode: boolean) => void;
5
5
  }
6
6
  /**
7
- * Implemented as a standalone Provider so that EditMode cna be implemented
8
- * at higher level than individual edit controls.
7
+ * Shares a view/edit mode flag between components that are rendered apart,
8
+ * e.g. a toolbar toggle and an edit form. Implemented as a standalone Provider
9
+ * so that edit mode can be controlled above the individual edit controls.
10
+ *
11
+ * `isEditMode` seeds (and resets) the mode; descendants can change it with
12
+ * `useEditMode().setEditMode`.
9
13
  */
10
14
  export declare const EditModeProvider: ({ children, isEditMode: isEditModeProp, }: {
11
15
  children: ReactNode;
12
16
  isEditMode?: boolean;
13
17
  }) => import("react").JSX.Element;
18
+ /**
19
+ * Returns the edit mode from the nearest EditModeProvider. With no provider,
20
+ * `isEditMode` is always false.
21
+ */
14
22
  export declare const useEditMode: () => EditModeContextProps;
@@ -34,6 +34,12 @@ export type NewRowState = {
34
34
  errors: Readonly<Record<string, string>>;
35
35
  submitting: boolean;
36
36
  values: Readonly<Record<string, VuuRowDataItemType>>;
37
+ /**
38
+ * Set when the server rejected the row. The message is reported against
39
+ * this column (the final column, where an inline add row shows it) and is
40
+ * cleared by the next change to the draft.
41
+ */
42
+ rowErrorColumn?: string;
37
43
  };
38
44
  export type EditLifecycle = {
39
45
  status: "idle";
@@ -78,11 +84,20 @@ export declare class EditSession extends EventEmitter<EditSessionEvents> impleme
78
84
  isNewRowFinalColumn(columnName: string): boolean;
79
85
  isNewRowComplete(): boolean;
80
86
  configureNewRow(columns: readonly string[], requiredColumns?: readonly string[]): void;
81
- setNewRowValue(column: string, value: VuuRowDataItemType): void;
87
+ setNewRowValue(column: string, value: VuuRowDataItemType | undefined): void;
88
+ setNewRowError(column: string, errorMessage?: string): void;
89
+ clearNewRowError(column: string): void;
90
+ /** Discards the new-row draft, leaving the rest of the session untouched. */
91
+ clearNewRow(): void;
82
92
  addNewRow(): Promise<RpcResult>;
83
93
  deleteSelectedRows(): Promise<RpcResult>;
84
94
  addRow(rowData?: Record<string, VuuRowDataItemType>): Promise<RpcResult>;
85
95
  hasRowChanges(key: string): boolean;
96
+ /**
97
+ * The edited values for a row, keyed by column name. Includes invalid
98
+ * edits. Returns an empty object when the row has no edits.
99
+ */
100
+ getEditedValues(key: string): Record<string, EditSessionValue>;
86
101
  isCellEdited(key: string, columnName: string): boolean;
87
102
  undoRowChange(key: string, action: EditActionType): Promise<RpcResult | undefined>;
88
103
  begin(copyOption?: CopyOption, sessionType?: SessionType): Promise<DataSource>;
@@ -92,6 +107,8 @@ export declare class EditSession extends EventEmitter<EditSessionEvents> impleme
92
107
  */
93
108
  reconcileWithSessionSchema(): void;
94
109
  get dataSource(): import("@vuu-ui/vuu-data-types").DataSourceBase<import("@vuu-ui/vuu-data-types").DataSourceRowWithBigint> | undefined;
110
+ /** The dataSource of the table being edited. */
111
+ get sourceDataSource(): DataSource | undefined;
95
112
  get sessionDataSource(): DataSource | undefined;
96
113
  end(saveChanges?: boolean, force?: boolean): Promise<void>;
97
114
  get lifecycle(): EditLifecycle;
@@ -0,0 +1,8 @@
1
+ import type { CommitHandler } from "@vuu-ui/vuu-utils";
2
+ export interface EditControlCheckboxProps {
3
+ className?: string;
4
+ onCommit: CommitHandler<HTMLElement>;
5
+ value?: unknown;
6
+ }
7
+ /** A checkbox that commits its value each time it is toggled. */
8
+ export declare const EditControlCheckbox: ({ className, onCommit, value, }: EditControlCheckboxProps) => import("react").JSX.Element;
@@ -0,0 +1,14 @@
1
+ import type { CommitHandler } from "@vuu-ui/vuu-utils";
2
+ export interface DropdownOption {
3
+ label: string;
4
+ value: string;
5
+ }
6
+ export interface EditControlDropdownProps {
7
+ className?: string;
8
+ onCommit: CommitHandler<HTMLElement>;
9
+ /** Either a list of values, or of options with distinct labels. */
10
+ values: readonly string[] | readonly DropdownOption[];
11
+ value?: unknown;
12
+ }
13
+ /** A dropdown that commits its value when an option is selected. */
14
+ export declare const EditControlDropdown: ({ className, onCommit, value, values, }: EditControlDropdownProps) => import("react").JSX.Element;
@@ -0,0 +1,13 @@
1
+ import type { LookupTableDetails } from "@vuu-ui/vuu-table-types";
2
+ import type { CommitHandler } from "@vuu-ui/vuu-utils";
3
+ export interface EditControlLookupDropdownProps {
4
+ className?: string;
5
+ lookup: LookupTableDetails;
6
+ onCommit: CommitHandler<HTMLElement>;
7
+ value?: unknown;
8
+ }
9
+ /**
10
+ * A dropdown offering the values of a lookup table, labelled with its
11
+ * `labelColumn`. Commits the `valueColumn` value of the selected row.
12
+ */
13
+ export declare const EditControlLookupDropdown: ({ className, lookup: { labelColumn, table, valueColumn }, onCommit, value, }: EditControlLookupDropdownProps) => import("react").JSX.Element;
@@ -0,0 +1,40 @@
1
+ import type { DataValueDescriptor, TableSchemaTable } from "@vuu-ui/vuu-data-types";
2
+ import { type VuuTypeaheadInputProps } from "@vuu-ui/vuu-ui-controls";
3
+ import { type CommitHandler } from "@vuu-ui/vuu-utils";
4
+ import type { InputProps } from "@salt-ds/core";
5
+ /**
6
+ * A rendering hint for the typeahead used in filters, shorthand for
7
+ * `minCharacterCountToTriggerSuggestions`.
8
+ * 'search' needs a character typed before suggestions show (1),
9
+ * 'pick' shows them on focus, like a dropdown (0).
10
+ */
11
+ export type DataItemEditControlVariant = "search" | "pick";
12
+ /**
13
+ * - "edit" (default): editing a value. Booleans are edited with a checkbox,
14
+ * and a typeahead is used only when the descriptor's `editor` asks for one.
15
+ * - "filter": entering a filter value. Every string column on a Vuu table
16
+ * gets a typeahead, suggesting the values present in the table.
17
+ */
18
+ export type DataItemEditControlPurpose = "edit" | "filter";
19
+ export interface DataItemEditControlProps {
20
+ InputProps?: Partial<InputProps>;
21
+ TypeaheadProps?: Pick<VuuTypeaheadInputProps, "highlightFirstSuggestion" | "minCharacterCountToTriggerSuggestions" | "selectOnTab">;
22
+ className?: string;
23
+ commitOnBlur?: boolean;
24
+ commitWhenCleared?: boolean;
25
+ /**
26
+ * A table column or form field Descriptor. Its `editor`, when present,
27
+ * determines the control.
28
+ */
29
+ dataDescriptor: DataValueDescriptor;
30
+ editOperation?: "insert" | "update";
31
+ errorMessage?: string;
32
+ onCommit: CommitHandler<HTMLElement>;
33
+ /** @default "edit" */
34
+ purpose?: DataItemEditControlPurpose;
35
+ /** The table the value belongs to. Required for a typeahead. */
36
+ table?: TableSchemaTable;
37
+ variant?: DataItemEditControlVariant;
38
+ }
39
+ export type ValidationStatus = "initial" | true | string;
40
+ export declare const getDataItemEditControl: ({ InputProps, TypeaheadProps, className, commitOnBlur, commitWhenCleared, dataDescriptor, editOperation, errorMessage, onCommit, purpose, table, variant, }: DataItemEditControlProps) => import("react").JSX.Element;
@@ -0,0 +1,4 @@
1
+ export * from "./EditControlCheckbox";
2
+ export * from "./EditControlDropdown";
3
+ export * from "./EditControlLookupDropdown";
4
+ export * from "./get-data-item-edit-control";
@@ -0,0 +1,9 @@
1
+ /** The edit operation that failed, passed to an `EditErrorHandler`. */
2
+ export type EditOperation = "begin" | "cancel" | "delete" | "save";
3
+ /**
4
+ * Called when an edit session operation fails. When no handler is supplied,
5
+ * hooks log the error to the console.
6
+ */
7
+ export type EditErrorHandler = (error: Error, operation: EditOperation) => void;
8
+ export declare const toError: (cause: unknown) => Error;
9
+ export declare const reportEditError: (source: string, onError: EditErrorHandler | undefined, cause: unknown, operation: EditOperation) => void;
@@ -1,16 +1,39 @@
1
1
  import type { DataRow } from "@vuu-ui/vuu-table-types";
2
- import type { VuuTable } from "@vuu-ui/vuu-protocol-types";
2
+ import type { DataValueDescriptor, DataValueValidationChecker } from "@vuu-ui/vuu-data-types";
3
+ import type { VuuColumnDataType, VuuTable } from "@vuu-ui/vuu-protocol-types";
3
4
  import { type OptionMap } from "../lookup-values/useLookupValues";
4
5
  import "./EditField.css";
5
6
  export type TextFieldType = "email" | "password";
6
7
  export type EditFieldType = "checkbox" | "dropdown" | TextFieldType;
7
8
  export interface EditFieldProps {
8
- dataRow: DataRow;
9
+ /**
10
+ * Client side validation, run on change and before commit. Invalid values
11
+ * are not sent to the server.
12
+ */
13
+ clientSideEditValidationCheck?: DataValueValidationChecker;
14
+ /**
15
+ * The row being edited. Use `{ key: EditSession.newRowKey }` for a new row.
16
+ * Defaults to the row of the enclosing `EditForm`.
17
+ */
18
+ dataRow?: DataRow;
19
+ /**
20
+ * For new rows only. When true, values are staged in the EditSession draft
21
+ * and the row is not added until `editSession.addNewRow()` is called (e.g.
22
+ * from a form Save button). When false, the row is added as soon as the
23
+ * final (or last required) field is committed. Defaults to true inside a
24
+ * create-mode `EditForm`, otherwise false.
25
+ */
9
26
  deferNewRow?: boolean;
10
27
  label: string;
11
28
  name: string;
12
29
  readOnly?: boolean;
13
30
  required?: boolean;
31
+ /**
32
+ * The server data type of the column. Determines how typed values are
33
+ * converted before commit. Defaults to "boolean" for checkbox fields,
34
+ * otherwise "string".
35
+ */
36
+ serverDataType?: VuuColumnDataType;
14
37
  type?: EditFieldType;
15
38
  }
16
39
  export interface DropdownEditFieldProps extends EditFieldProps {
@@ -18,4 +41,15 @@ export interface DropdownEditFieldProps extends EditFieldProps {
18
41
  optionMap: OptionMap;
19
42
  table: VuuTable;
20
43
  }
21
- export declare const EditField: ({ dataRow, deferNewRow, label, name, readOnly, required, type, ...options }: EditFieldProps | DropdownEditFieldProps) => import("react").JSX.Element;
44
+ /**
45
+ * A field described by a column descriptor, the same descriptor a Table
46
+ * column uses. The control is chosen by `getDataItemEditControl`, from the
47
+ * descriptor's `editor` and `serverDataType`. `editable` and `required` are
48
+ * respected.
49
+ */
50
+ export interface ColumnEditFieldProps extends Omit<EditFieldProps, "label" | "name" | "readOnly" | "required" | "serverDataType" | "type"> {
51
+ column: DataValueDescriptor;
52
+ /** Defaults to `column.label`, then `column.name` */
53
+ label?: string;
54
+ }
55
+ export declare const EditField: (props: EditFieldProps | DropdownEditFieldProps | ColumnEditFieldProps) => import("react").JSX.Element;
@@ -4,13 +4,14 @@ import { type ChangeEventHandler, type FocusEventHandler, type KeyboardEvent, ty
4
4
  import type { LookupOption } from "../lookup-values/useLookupValues";
5
5
  import type { TextFieldType } from "./EditField";
6
6
  export interface EditFieldHookProps<T extends VuuRowDataItemType = VuuRowDataItemType> {
7
- column: Pick<RuntimeColumnDescriptor, "clientSideEditValidationCheck" | "label" | "name">;
7
+ column: Pick<RuntimeColumnDescriptor, "clientSideEditValidationCheck" | "label" | "name" | "required">;
8
8
  value?: T;
9
9
  onEdit?: TableCellEditHandler;
10
10
  textType?: TextFieldType;
11
11
  type?: "string" | "number" | "boolean";
12
12
  }
13
13
  export declare const useEditField: <T extends string | number | boolean = string>({ column, value, onEdit, textType, type, }: EditFieldHookProps<T>) => {
14
+ commit: (value?: string) => Promise<boolean>;
14
15
  editing: boolean;
15
16
  inputProps: {
16
17
  onBlur: FocusEventHandler<HTMLElement>;
@@ -0,0 +1,29 @@
1
+ import type { DataValueDescriptor } from "@vuu-ui/vuu-data-types";
2
+ import { type ReactNode } from "react";
3
+ import { type EditFormProps } from "./EditForm";
4
+ /**
5
+ * A create form field. The control is chosen from its `editor` and
6
+ * `serverDataType` by `getDataItemEditControl`. Required unless
7
+ * `required: false`.
8
+ */
9
+ export type CreateRowFormField = DataValueDescriptor;
10
+ export interface CreateRowFormProps extends Omit<EditFormProps, "children" | "columns" | "dataRow" | "isEditMode" | "mode" | "requiredColumns"> {
11
+ /** Rendered after the fields. */
12
+ children?: ReactNode;
13
+ /**
14
+ * Fields, in tab order. Every field is required unless it sets
15
+ * `required: false`. Omit to compose custom editors as `children`, which
16
+ * stage their values with `setValue` from `useEditFormContext`.
17
+ */
18
+ fields?: readonly CreateRowFormField[];
19
+ }
20
+ /**
21
+ * A form that adds one row to `dataSource`, or, given an `editSession`, to
22
+ * that session. Renders an `EditField` per entry in `fields`, stages the values as a new-row draft, and adds the row on
23
+ * Create. Required fields left empty, `validate` errors and server
24
+ * rejections are shown on the form; the row is only added once all pass.
25
+ *
26
+ * The form is single-use: close it from `onSaved`/`onCancelled`, or remount
27
+ * it (change its `key`) to create another row.
28
+ */
29
+ export declare const CreateRowForm: ({ children, fields, ...props }: CreateRowFormProps) => import("react").JSX.Element;
@@ -0,0 +1,36 @@
1
+ import { type HTMLAttributes, type ReactNode } from "react";
2
+ import { type EditFormHookProps, type EditFormHookResult } from "../useEditForm";
3
+ import "./EditForm.css";
4
+ export interface EditFormProps extends EditFormHookProps, Omit<HTMLAttributes<HTMLFormElement>, "children" | "onError" | "onSubmit" | "title"> {
5
+ /**
6
+ * Form fields, typically `EditField`s. Pass a function to receive the form
7
+ * state, e.g. to render fields conditionally.
8
+ */
9
+ children: ReactNode | ((form: EditFormHookResult) => ReactNode);
10
+ /** Return false (or resolve false) to keep editing. See `useConfirmDiscard`. */
11
+ confirmCancel?: () => boolean | Promise<boolean>;
12
+ /** Hide the Cancel button. */
13
+ hideCancel?: boolean;
14
+ /**
15
+ * @default "Save" in edit mode, "Done" with direct editing, "Create" in
16
+ * create mode, or "Add" when adding to a shared `editSession`
17
+ */
18
+ saveLabel?: string;
19
+ /**
20
+ * Set false to render the buttons yourself, with `EditFormButtons`.
21
+ * @default true
22
+ */
23
+ showButtons?: boolean;
24
+ title?: ReactNode;
25
+ }
26
+ /**
27
+ * A form that edits one row of `dataSource`, or creates a new one. Owns the
28
+ * EditSession (via `useEditForm`), provides it to child `EditField`s, and
29
+ * renders an error banner plus Save/Cancel buttons.
30
+ *
31
+ * In edit mode, fields are editable while `isEditMode` is true (or the
32
+ * enclosing `EditModeProvider` is in edit mode). Create mode is always
33
+ * editable. `EditField`s pick up the form's row (a new-row placeholder in
34
+ * create mode), so need no `dataRow` prop.
35
+ */
36
+ export declare const EditForm: ({ children, className, columns, confirmCancel, dataRow, dataSource, deleteMode, editModel, editSession: sharedEditSession, editSessionApi, hideCancel, isEditMode, mode, onCancelled, onError, onSaved, requiredColumns, rowDefaults, saveLabel, showButtons, title, validate, ...htmlAttributes }: EditFormProps) => import("react").JSX.Element;
@@ -0,0 +1,18 @@
1
+ export interface EditFormButtonsProps {
2
+ /** Return false (or resolve false) to keep editing. See `useConfirmDiscard`. */
3
+ confirmCancel?: () => boolean | Promise<boolean>;
4
+ /** Hide the Cancel button. */
5
+ hideCancel?: boolean;
6
+ /**
7
+ * @default "Save" in edit mode, "Done" with direct editing, "Create" in
8
+ * create mode, or "Add" when adding to a shared session
9
+ */
10
+ saveLabel?: string;
11
+ }
12
+ /**
13
+ * Save (or Create) and Cancel buttons for the enclosing `EditForm`. The form
14
+ * renders these itself while editing; render them yourself, with
15
+ * `showButtons={false}` on the form, to place them elsewhere in the form or
16
+ * to show them, disabled, outside edit mode.
17
+ */
18
+ export declare const EditFormButtons: ({ confirmCancel, hideCancel, saveLabel: saveLabelProp, }: EditFormButtonsProps) => import("react").JSX.Element;
@@ -0,0 +1,59 @@
1
+ export declare const EditFormContext: import("react").Context<{
2
+ canCancel: boolean;
3
+ editState: import("..").EditState;
4
+ inEditMode: boolean;
5
+ isActive: boolean;
6
+ isEnding: boolean;
7
+ isStarting: boolean;
8
+ lifecycle: import("..").EditLifecycle;
9
+ newRowState: import("..").NewRowState;
10
+ canSave: boolean;
11
+ cancel: () => Promise<void>;
12
+ dataRow: import("@vuu-ui/vuu-table-types").DataRow | undefined;
13
+ editModel: "direct" | "session";
14
+ editSession: import("..").EditSession;
15
+ isSharedSession: boolean;
16
+ fieldEditSession: import("..").DirectEditSession | import("..").EditSession;
17
+ error: Error | undefined;
18
+ fieldErrors: Readonly<Record<string, string>>;
19
+ getValues: () => import("..").EditFormValues;
20
+ isDirty: boolean;
21
+ isEditMode: boolean;
22
+ mode: import("..").EditFormMode;
23
+ rowAdded: boolean;
24
+ saving: boolean;
25
+ setValue: (name: string, value: import("@vuu-ui/vuu-protocol-types").VuuRowDataItemType | undefined) => void;
26
+ submit: (force?: boolean) => Promise<boolean>;
27
+ } | undefined>;
28
+ /**
29
+ * Returns the state of the enclosing `EditForm`, or undefined outside a form.
30
+ * Use it in custom field components to read `fieldErrors`, `mode`, `saving`
31
+ * etc.
32
+ */
33
+ export declare const useEditFormContext: () => {
34
+ canCancel: boolean;
35
+ editState: import("..").EditState;
36
+ inEditMode: boolean;
37
+ isActive: boolean;
38
+ isEnding: boolean;
39
+ isStarting: boolean;
40
+ lifecycle: import("..").EditLifecycle;
41
+ newRowState: import("..").NewRowState;
42
+ canSave: boolean;
43
+ cancel: () => Promise<void>;
44
+ dataRow: import("@vuu-ui/vuu-table-types").DataRow | undefined;
45
+ editModel: "direct" | "session";
46
+ editSession: import("..").EditSession;
47
+ isSharedSession: boolean;
48
+ fieldEditSession: import("..").DirectEditSession | import("..").EditSession;
49
+ error: Error | undefined;
50
+ fieldErrors: Readonly<Record<string, string>>;
51
+ getValues: () => import("..").EditFormValues;
52
+ isDirty: boolean;
53
+ isEditMode: boolean;
54
+ mode: import("..").EditFormMode;
55
+ rowAdded: boolean;
56
+ saving: boolean;
57
+ setValue: (name: string, value: import("@vuu-ui/vuu-protocol-types").VuuRowDataItemType | undefined) => void;
58
+ submit: (force?: boolean) => Promise<boolean>;
59
+ } | undefined;