@dxos/react-ui-table 0.8.3 → 0.8.4-main.03d5cd7b56

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 (194) hide show
  1. package/dist/lib/browser/chunk-FBURZBPH.mjs +70 -0
  2. package/dist/lib/browser/chunk-FBURZBPH.mjs.map +7 -0
  3. package/dist/lib/browser/chunk-J5LGTIGS.mjs +10 -0
  4. package/dist/lib/browser/chunk-J5LGTIGS.mjs.map +7 -0
  5. package/dist/lib/browser/chunk-YGC7LZJN.mjs +1695 -0
  6. package/dist/lib/browser/chunk-YGC7LZJN.mjs.map +7 -0
  7. package/dist/lib/browser/index.mjs +1162 -2554
  8. package/dist/lib/browser/index.mjs.map +4 -4
  9. package/dist/lib/browser/meta.json +1 -1
  10. package/dist/lib/browser/testing/index.mjs +115 -0
  11. package/dist/lib/browser/testing/index.mjs.map +7 -0
  12. package/dist/lib/browser/translations.mjs +36 -0
  13. package/dist/lib/browser/translations.mjs.map +7 -0
  14. package/dist/lib/browser/types/index.mjs +4 -5
  15. package/dist/lib/node-esm/chunk-DKKMSVKP.mjs +71 -0
  16. package/dist/lib/node-esm/chunk-DKKMSVKP.mjs.map +7 -0
  17. package/dist/lib/node-esm/chunk-GKIWDEZ7.mjs +1697 -0
  18. package/dist/lib/node-esm/chunk-GKIWDEZ7.mjs.map +7 -0
  19. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +11 -0
  20. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +7 -0
  21. package/dist/lib/node-esm/index.mjs +1162 -2554
  22. package/dist/lib/node-esm/index.mjs.map +4 -4
  23. package/dist/lib/node-esm/meta.json +1 -1
  24. package/dist/lib/node-esm/testing/index.mjs +116 -0
  25. package/dist/lib/node-esm/testing/index.mjs.map +7 -0
  26. package/dist/lib/node-esm/translations.mjs +37 -0
  27. package/dist/lib/node-esm/translations.mjs.map +7 -0
  28. package/dist/lib/node-esm/types/index.mjs +4 -5
  29. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts +2 -3
  30. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts.map +1 -1
  31. package/dist/types/src/components/Table/ColumnSettings.d.ts +4 -2
  32. package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
  33. package/dist/types/src/components/Table/DynamicTable.d.ts +7 -7
  34. package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
  35. package/dist/types/src/components/Table/DynamicTable.stories.d.ts +35 -3
  36. package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
  37. package/dist/types/src/components/Table/Relations.stories.d.ts +111 -0
  38. package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -0
  39. package/dist/types/src/components/Table/RowActionsMenu.d.ts +1 -1
  40. package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
  41. package/dist/types/src/components/Table/Table.d.ts +21 -18
  42. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  43. package/dist/types/src/components/Table/Table.stories.d.ts +110 -6
  44. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  45. package/dist/types/src/components/Table/TableContent.d.ts +17 -0
  46. package/dist/types/src/components/Table/TableContent.d.ts.map +1 -0
  47. package/dist/types/src/components/Table/TableToolbar.d.ts +21 -0
  48. package/dist/types/src/components/Table/TableToolbar.d.ts.map +1 -0
  49. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts +2 -1
  50. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts.map +1 -1
  51. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +14 -9
  52. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
  53. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +12 -18
  54. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  55. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +38 -5
  56. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  57. package/dist/types/src/components/TableCellEditor/index.d.ts +1 -0
  58. package/dist/types/src/components/TableCellEditor/index.d.ts.map +1 -1
  59. package/dist/types/src/components/index.d.ts +0 -1
  60. package/dist/types/src/components/index.d.ts.map +1 -1
  61. package/dist/types/src/components/testing.d.ts +26 -56
  62. package/dist/types/src/components/testing.d.ts.map +1 -1
  63. package/dist/types/src/hooks/index.d.ts +2 -0
  64. package/dist/types/src/hooks/index.d.ts.map +1 -1
  65. package/dist/types/src/hooks/useAddRow.d.ts +13 -0
  66. package/dist/types/src/hooks/useAddRow.d.ts.map +1 -0
  67. package/dist/types/src/hooks/useProjectionModel.d.ts +6 -0
  68. package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -0
  69. package/dist/types/src/hooks/useTableModel.d.ts +11 -10
  70. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  71. package/dist/types/src/index.d.ts +0 -3
  72. package/dist/types/src/index.d.ts.map +1 -1
  73. package/dist/types/src/model/modal-controller.d.ts +5 -8
  74. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  75. package/dist/types/src/model/selection-model.d.ts +18 -8
  76. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  77. package/dist/types/src/model/table-model.d.ts +150 -32
  78. package/dist/types/src/model/table-model.d.ts.map +1 -1
  79. package/dist/types/src/model/table-presentation.d.ts +13 -2
  80. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  81. package/dist/types/src/playwright/TableManager.d.ts +2 -1
  82. package/dist/types/src/playwright/TableManager.d.ts.map +1 -1
  83. package/dist/types/src/playwright/playwright.config.d.ts +3 -0
  84. package/dist/types/src/playwright/playwright.config.d.ts.map +1 -0
  85. package/dist/types/src/testing/index.d.ts +2 -0
  86. package/dist/types/src/testing/index.d.ts.map +1 -0
  87. package/dist/types/src/testing/useTestTableModel.d.ts +25 -0
  88. package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -0
  89. package/dist/types/src/translations.d.ts +22 -25
  90. package/dist/types/src/translations.d.ts.map +1 -1
  91. package/dist/types/src/types/Table.d.ts +26 -0
  92. package/dist/types/src/types/Table.d.ts.map +1 -0
  93. package/dist/types/src/types/index.d.ts +1 -1
  94. package/dist/types/src/types/index.d.ts.map +1 -1
  95. package/dist/types/src/util/dynamic-table.d.ts +15 -14
  96. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  97. package/dist/types/src/util/dynamic-table.test.d.ts +2 -0
  98. package/dist/types/src/util/dynamic-table.test.d.ts.map +1 -0
  99. package/dist/types/src/util/index.d.ts +2 -1
  100. package/dist/types/src/util/index.d.ts.map +1 -1
  101. package/dist/types/src/util/query-ast.d.ts +7 -0
  102. package/dist/types/src/util/query-ast.d.ts.map +1 -0
  103. package/dist/types/src/util/schema.d.ts +1 -1
  104. package/dist/types/src/util/schema.d.ts.map +1 -1
  105. package/dist/types/src/util/signals.d.ts.map +1 -1
  106. package/dist/types/src/util/sort.d.ts +12 -0
  107. package/dist/types/src/util/sort.d.ts.map +1 -0
  108. package/dist/types/src/util/table-buttons.d.ts +31 -17
  109. package/dist/types/src/util/table-buttons.d.ts.map +1 -1
  110. package/dist/types/src/util/table-controls.d.ts +4 -4
  111. package/dist/types/src/util/table-controls.d.ts.map +1 -1
  112. package/dist/types/src/util/tag.d.ts.map +1 -1
  113. package/dist/types/tsconfig.tsbuildinfo +1 -1
  114. package/package.json +76 -72
  115. package/src/components/Table/ColumnActionsMenu.tsx +18 -13
  116. package/src/components/Table/ColumnSettings.tsx +18 -15
  117. package/src/components/Table/DynamicTable.stories.tsx +40 -36
  118. package/src/components/Table/DynamicTable.tsx +38 -34
  119. package/src/components/Table/Relations.stories.tsx +281 -0
  120. package/src/components/Table/RowActionsMenu.tsx +29 -19
  121. package/src/components/Table/Table.stories.tsx +198 -214
  122. package/src/components/Table/Table.tsx +47 -363
  123. package/src/components/Table/TableContent.tsx +421 -0
  124. package/src/components/Table/TableToolbar.tsx +111 -0
  125. package/src/components/TableCellEditor/CellValidationMessage.tsx +9 -5
  126. package/src/components/TableCellEditor/FormCellEditor.tsx +153 -49
  127. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +40 -79
  128. package/src/components/TableCellEditor/TableCellEditor.tsx +109 -199
  129. package/src/components/TableCellEditor/index.ts +1 -0
  130. package/src/components/index.ts +0 -1
  131. package/src/components/testing.ts +46 -37
  132. package/src/hooks/index.ts +2 -0
  133. package/src/hooks/useAddRow.ts +36 -0
  134. package/src/hooks/useProjectionModel.ts +39 -0
  135. package/src/hooks/useTableModel.ts +41 -35
  136. package/src/index.ts +0 -5
  137. package/src/model/modal-controller.ts +24 -41
  138. package/src/model/selection-model.ts +75 -41
  139. package/src/model/table-model.test.ts +91 -56
  140. package/src/model/table-model.ts +629 -162
  141. package/src/model/table-presentation.test.ts +69 -59
  142. package/src/model/table-presentation.ts +283 -154
  143. package/src/playwright/TableManager.ts +13 -6
  144. package/src/playwright/playwright.config.ts +17 -0
  145. package/src/playwright/smoke.spec.ts +181 -42
  146. package/src/testing/index.ts +5 -0
  147. package/src/testing/useTestTableModel.ts +133 -0
  148. package/src/translations.ts +24 -24
  149. package/src/types/Table.ts +79 -0
  150. package/src/types/index.ts +2 -2
  151. package/src/util/dynamic-table.test.ts +70 -0
  152. package/src/util/dynamic-table.ts +51 -36
  153. package/src/util/index.ts +2 -1
  154. package/src/util/query-ast.ts +22 -0
  155. package/src/util/schema.ts +4 -4
  156. package/src/util/sort.ts +90 -0
  157. package/src/util/table-buttons.ts +28 -4
  158. package/src/util/table-controls.ts +6 -3
  159. package/dist/lib/browser/chunk-TL7CKQQV.mjs +0 -27
  160. package/dist/lib/browser/chunk-TL7CKQQV.mjs.map +0 -7
  161. package/dist/lib/node/chunk-O2VPBNS5.cjs +0 -50
  162. package/dist/lib/node/chunk-O2VPBNS5.cjs.map +0 -7
  163. package/dist/lib/node/index.cjs +0 -2739
  164. package/dist/lib/node/index.cjs.map +0 -7
  165. package/dist/lib/node/meta.json +0 -1
  166. package/dist/lib/node/types/index.cjs +0 -31
  167. package/dist/lib/node/types/index.cjs.map +0 -7
  168. package/dist/lib/node-esm/chunk-GBT6GD4G.mjs +0 -29
  169. package/dist/lib/node-esm/chunk-GBT6GD4G.mjs.map +0 -7
  170. package/dist/types/src/components/Table/CreateRefPanel.d.ts +0 -9
  171. package/dist/types/src/components/Table/CreateRefPanel.d.ts.map +0 -1
  172. package/dist/types/src/components/Table/relations.stories.d.ts +0 -9
  173. package/dist/types/src/components/Table/relations.stories.d.ts.map +0 -1
  174. package/dist/types/src/components/TableCellEditor/extension.d.ts +0 -13
  175. package/dist/types/src/components/TableCellEditor/extension.d.ts.map +0 -1
  176. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +0 -21
  177. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +0 -1
  178. package/dist/types/src/components/TableToolbar/index.d.ts +0 -2
  179. package/dist/types/src/components/TableToolbar/index.d.ts.map +0 -1
  180. package/dist/types/src/model/table-sorting.d.ts +0 -53
  181. package/dist/types/src/model/table-sorting.d.ts.map +0 -1
  182. package/dist/types/src/types/table.d.ts +0 -56
  183. package/dist/types/src/types/table.d.ts.map +0 -1
  184. package/dist/types/src/util/table.d.ts +0 -14
  185. package/dist/types/src/util/table.d.ts.map +0 -1
  186. package/src/components/Table/CreateRefPanel.tsx +0 -84
  187. package/src/components/Table/relations.stories.tsx +0 -124
  188. package/src/components/TableCellEditor/extension.ts +0 -61
  189. package/src/components/TableToolbar/TableToolbar.tsx +0 -102
  190. package/src/components/TableToolbar/index.ts +0 -5
  191. package/src/model/table-sorting.ts +0 -216
  192. package/src/playwright/playwright.config.cts +0 -18
  193. package/src/types/table.ts +0 -27
  194. package/src/util/table.ts +0 -127
@@ -3,64 +3,56 @@
3
3
  //
4
4
 
5
5
  import { type Completion } from '@codemirror/autocomplete';
6
- import { type Schema } from 'effect/Schema';
6
+ import { EditorView } from '@codemirror/view';
7
7
  import React, { useCallback, useMemo, useRef, useState } from 'react';
8
8
 
9
- import { FormatEnum, TypeEnum } from '@dxos/echo-schema';
9
+ import { debounce } from '@dxos/async';
10
+ import { type Type } from '@dxos/echo';
11
+ import { Format, TypeEnum } from '@dxos/echo/internal';
10
12
  import { invariant } from '@dxos/invariant';
11
- import { type DxGrid } from '@dxos/lit-grid';
12
- import { useThemeContext } from '@dxos/react-ui';
13
- import { createMarkdownExtensions } from '@dxos/react-ui-editor';
13
+ import { type DxGridAxis, type DxGridPosition } from '@dxos/lit-grid';
14
14
  import {
15
- cellQuery,
16
- editorKeys,
17
- parseCellIndex,
18
- useGridContext,
19
- type DxGridPlanePosition,
15
+ type EditorBlurHandler,
20
16
  type EditorKeyEvent,
21
17
  type EditorKeyOrBlurHandler,
22
- type EditorBlurHandler,
23
18
  GridCellEditor,
24
19
  type GridCellEditorProps,
25
20
  type GridScopedProps,
21
+ editorKeys,
22
+ parseCellIndex,
23
+ useGridContext,
26
24
  } from '@dxos/react-ui-grid';
27
- import { tagPickerExtension, createLinks } from '@dxos/react-ui-tag-picker';
28
25
  import { type FieldProjection } from '@dxos/schema';
29
26
 
27
+ import { type ModalController, type TableModel } from '../../model';
30
28
  import { CellValidationMessage } from './CellValidationMessage';
31
- import { FormCellEditor } from './FormCellEditor';
32
- import { completion } from './extension';
33
- import { type TableModel, type ModalController } from '../../model';
34
-
35
- const newValue = Symbol.for('newValue');
29
+ import { FormCellEditor, type OnCreateHandler } from './FormCellEditor';
36
30
 
37
31
  /**
38
32
  * Option to create new object/value.
39
33
  */
40
- export const createOption = (text: string) => ({ [newValue]: true, text });
41
-
42
- const isCreateOption = (data: any) => typeof data === 'object' && data[newValue];
43
-
44
- export type QueryResult = Pick<Completion, 'label'> & { data: any };
34
+ export type QueryResult = Pick<Completion, 'label'> & {
35
+ data: any;
36
+ };
45
37
 
46
- export type TableCellEditorProps = {
38
+ export type TableCellEditorProps<T extends Type.AnyEntity = Type.AnyEntity> = {
39
+ schema?: T;
47
40
  model?: TableModel;
48
41
  modals?: ModalController;
49
- schema?: Schema.AnyNoContext;
50
- onEnter?: (cell: DxGridPlanePosition) => void;
51
- onFocus?: DxGrid['refocus'];
52
- onQuery?: (field: FieldProjection, text: string) => Promise<QueryResult[]>;
42
+ onFocus?: (axis?: DxGridAxis, delta?: -1 | 0 | 1, cell?: DxGridPosition) => void;
43
+ onCreate?: OnCreateHandler;
44
+ onSave?: () => void;
53
45
  };
54
46
 
55
- export const TableValueEditor = ({
47
+ export const TableValueEditor = <T extends Type.AnyEntity = Type.AnyEntity>({
48
+ __gridScope,
49
+ schema,
56
50
  model,
57
51
  modals,
58
- schema,
59
- onEnter,
60
52
  onFocus,
61
- onQuery,
62
- __gridScope,
63
- }: GridScopedProps<TableCellEditorProps>) => {
53
+ onSave,
54
+ onCreate,
55
+ }: GridScopedProps<TableCellEditorProps<T>>) => {
64
56
  const { editing } = useGridContext('TableValueEditor', __gridScope);
65
57
 
66
58
  const fieldProjection = useMemo<FieldProjection | undefined>(() => {
@@ -69,41 +61,46 @@ export const TableValueEditor = ({
69
61
  }
70
62
 
71
63
  const { col } = parseCellIndex(editing.index);
72
- const field = model.projection.view.fields[col];
64
+ const field = model.projection.getFields()[col];
73
65
  const fieldProjection = model.projection.getFieldProjection(field.id);
74
66
  invariant(fieldProjection);
75
67
  return fieldProjection;
76
68
  }, [model, editing]);
77
69
 
78
- if (fieldProjection?.props.type === TypeEnum.Array) {
79
- return <FormCellEditor fieldProjection={fieldProjection} model={model} schema={schema} __gridScope={__gridScope} />;
70
+ if (
71
+ fieldProjection?.props.type === TypeEnum.Array ||
72
+ fieldProjection?.props.format === Format.TypeFormat.SingleSelect ||
73
+ fieldProjection?.props.format === Format.TypeFormat.Ref
74
+ // TODO(thure): Support `Format.TypeFormat.MultiSelect`
75
+ ) {
76
+ return (
77
+ <FormCellEditor<T>
78
+ __gridScope={__gridScope}
79
+ schema={schema}
80
+ model={model}
81
+ fieldProjection={fieldProjection}
82
+ modals={modals}
83
+ onSave={onSave}
84
+ onCreate={onCreate}
85
+ />
86
+ );
80
87
  }
81
88
 
82
- // For all other types, use the existing cell editor
83
- return (
84
- <TableCellEditor
85
- model={model}
86
- modals={modals}
87
- onEnter={onEnter}
88
- onFocus={onFocus}
89
- onQuery={onQuery}
90
- __gridScope={__gridScope}
91
- />
92
- );
89
+ // For all other types, use the existing cell editor.
90
+ return <TableCellEditor model={model} modals={modals} onFocus={onFocus} onSave={onSave} __gridScope={__gridScope} />;
93
91
  };
94
92
 
95
93
  export const TableCellEditor = ({
94
+ __gridScope,
96
95
  model,
97
96
  modals,
98
- onEnter,
99
97
  onFocus,
100
- onQuery,
101
- __gridScope,
98
+ onSave,
102
99
  }: GridScopedProps<TableCellEditorProps>) => {
103
- const { id: gridId, editing, setEditing } = useGridContext('TableCellEditor', __gridScope);
100
+ const { editing, setEditing } = useGridContext('TableCellEditor', __gridScope);
104
101
  const suppressNextBlur = useRef(false);
105
- const { themeMode } = useThemeContext();
106
- const [_validationError, setValidationError] = useState<string | null>(null);
102
+ const [validationError, setValidationError] = useState<string | null>(null);
103
+ const [validationVariant, setValidationVariant] = useState<'error' | 'warning'>('error');
107
104
 
108
105
  const fieldProjection = useMemo<FieldProjection | undefined>(() => {
109
106
  if (!model || !editing) {
@@ -111,12 +108,13 @@ export const TableCellEditor = ({
111
108
  }
112
109
 
113
110
  const { col } = parseCellIndex(editing.index);
114
- const field = model.projection.view.fields[col];
111
+ const field = model.projection.getFields()[col];
115
112
  const fieldProjection = model.projection.getFieldProjection(field.id);
116
113
  invariant(fieldProjection);
117
114
  return fieldProjection;
118
115
  }, [model, editing]);
119
116
 
117
+ // TOOD(burdon): Attach to event handler?
120
118
  const handleEnter = useCallback(
121
119
  async (value: any) => {
122
120
  if (!model || !editing) {
@@ -124,34 +122,39 @@ export const TableCellEditor = ({
124
122
  }
125
123
 
126
124
  const cell = parseCellIndex(editing.index);
125
+ const validationResult = await model.validateCellData(cell, value);
127
126
 
128
- // Validate the value
129
- const result = await model.validateCellData(cell, value);
130
-
131
- if (result.valid) {
127
+ if (validationResult.valid) {
132
128
  setValidationError(null);
133
129
  model.setCellData(cell, value);
134
- onEnter?.(cell);
135
130
  onFocus?.();
136
131
  setEditing(null);
132
+ onSave?.();
137
133
  } else {
138
- setValidationError(result.error);
139
- // Editor stays open on validation failure
134
+ setValidationError(validationResult.error);
135
+ setValidationVariant('error');
140
136
  }
141
137
  },
142
- [model, editing, onEnter, onFocus, setEditing],
138
+ [model, editing, onFocus, setEditing],
143
139
  );
144
140
 
145
141
  const handleBlur = useCallback<EditorBlurHandler>(
146
142
  async (value) => {
147
- if (!model || !editing) {
148
- return;
149
- }
150
143
  if (suppressNextBlur.current) {
151
144
  suppressNextBlur.current = false;
145
+ } else if (model && editing) {
146
+ // Save silently if validation passes
147
+ const cell = parseCellIndex(editing.index);
148
+ if (value !== undefined) {
149
+ const result = await model.validateCellData(cell, value);
150
+ if (result.valid) {
151
+ setValidationError(null);
152
+ model.setCellData(cell, value);
153
+ setEditing(null);
154
+ onSave?.();
155
+ }
156
+ }
152
157
  }
153
-
154
- // Don't save on blur - let handleClose handle validation and saving
155
158
  },
156
159
  [model, editing],
157
160
  );
@@ -163,177 +166,75 @@ export const TableCellEditor = ({
163
166
  }
164
167
 
165
168
  const cell = parseCellIndex(editing.index);
166
-
167
169
  if (value !== undefined) {
168
170
  // Pre-commit validation check.
169
171
  const result = await model.validateCellData(cell, value);
170
172
 
171
173
  if (result.valid) {
174
+ suppressNextBlur.current = true;
172
175
  setValidationError(null);
173
176
  model.setCellData(cell, value);
174
177
  setEditing(null);
175
- onEnter?.(cell);
178
+ onSave?.();
176
179
  if (event && onFocus) {
177
- onFocus(determineNavigationAxis(event), determineNavigationDelta(event));
180
+ onFocus(determineNavigationAxis(event), determineNavigationDelta(event), cell);
178
181
  }
179
182
  } else {
180
183
  setValidationError(result.error);
184
+ setValidationVariant('error');
181
185
  }
182
186
  } else {
187
+ suppressNextBlur.current = true;
183
188
  setValidationError(null);
184
189
  setEditing(null);
190
+ onSave?.();
185
191
  if (event && onFocus) {
186
192
  onFocus(determineNavigationAxis(event), determineNavigationDelta(event));
187
193
  }
188
194
  }
189
195
  },
190
- [model, editing, onFocus, onEnter, fieldProjection, setEditing],
196
+ [model, editing, onFocus, fieldProjection, setEditing, onSave],
191
197
  );
192
198
 
193
- const extension = useMemo(() => {
199
+ const extensions = useMemo(() => {
194
200
  if (!fieldProjection) {
195
201
  return [];
196
202
  }
197
203
 
198
- const extension = [
204
+ const extensions = [
199
205
  editorKeys({
200
206
  onClose: handleClose,
201
207
  ...(editing?.initialContent && { onNav: handleClose }),
202
208
  }),
203
209
  ];
204
-
205
- const format = fieldProjection.props.format;
206
-
207
- if (format === FormatEnum.SingleSelect || format === FormatEnum.MultiSelect) {
208
- // TODO(ZaymonFC): Reconcile this with the TagPicker component?
209
- // Add markdown extensions needed by tag picker.
210
- extension.push(createMarkdownExtensions({ themeMode }));
211
-
212
- const options = fieldProjection.props.options || [];
213
-
214
- const mode = format === FormatEnum.SingleSelect ? ('single-select' as const) : ('multi-select' as const);
215
-
216
- extension.push(
217
- tagPickerExtension({
218
- mode,
219
- inGrid: true,
220
- onSearch: (text, selectedIds) => {
221
- return options
222
- .filter(
223
- (option) =>
224
- selectedIds.indexOf(option.id) === -1 &&
225
- (text.length === 0 || option.title.toLowerCase().includes(text.toLowerCase())),
226
- )
227
- .map((option) => ({
228
- id: option.id,
229
- label: option.title,
230
- hue: option.color as any,
231
- }));
232
- },
233
- onUpdate: (ids) => {
234
- if (model && editing) {
235
- if (ids.length === 0) {
236
- return;
237
- }
238
- if (mode === 'single-select') {
239
- void handleEnter(ids[0]);
210
+ // Add validation extension to handle content changes.
211
+ if (model && editing) {
212
+ extensions.push(
213
+ EditorView.updateListener.of(
214
+ debounce((update) => {
215
+ const content = update.state.doc.toString();
216
+ const cell = parseCellIndex(editing.index);
217
+
218
+ // Perform validation on content change.
219
+ void model.validateCellData(cell, content).then((result) => {
220
+ if (result.valid) {
221
+ setValidationError(null);
240
222
  } else {
241
- void handleEnter(ids);
223
+ setValidationError(result.error);
224
+ setValidationVariant('error');
242
225
  }
243
- }
244
- },
245
- }),
226
+ });
227
+ }, 10),
228
+ ),
246
229
  );
247
230
  }
248
231
 
249
- if (onQuery) {
250
- switch (fieldProjection.props.format) {
251
- case FormatEnum.Ref: {
252
- extension.push([
253
- completion({
254
- onQuery: (text) => onQuery(fieldProjection, text),
255
- onMatch: (data) => {
256
- if (model && editing && modals) {
257
- if (isCreateOption(data)) {
258
- const { field, props } = fieldProjection;
259
- if (props.referenceSchema) {
260
- suppressNextBlur.current = true;
261
- modals.openCreateRef(
262
- props.referenceSchema,
263
- document.querySelector(cellQuery(editing.index, gridId)),
264
- {
265
- [field.referencePath!]: data.text,
266
- },
267
- (data) => {
268
- void handleEnter(data);
269
- },
270
- );
271
- }
272
- } else {
273
- void handleEnter(data);
274
- }
275
- }
276
- },
277
- }),
278
- ]);
279
- break;
280
- }
281
- }
282
- }
283
-
284
- return extension;
285
- }, [model, modals, editing, fieldProjection, handleClose, themeMode]);
232
+ return extensions;
233
+ }, [model, modals, editing, fieldProjection, handleClose]);
286
234
 
287
235
  const getCellContent = useCallback<GridCellEditorProps['getCellContent']>(() => {
288
236
  if (model && editing) {
289
237
  const cell = parseCellIndex(editing.index);
290
- const { col } = cell;
291
- const field = model.projection.view.fields[col];
292
- const fieldProjection = model.projection.getFieldProjection(field.id);
293
-
294
- if (
295
- fieldProjection?.props.format === FormatEnum.SingleSelect ||
296
- fieldProjection?.props.format === FormatEnum.MultiSelect
297
- ) {
298
- const value = model.getCellData(cell);
299
-
300
- if (value !== undefined) {
301
- const options = fieldProjection.props.options || [];
302
-
303
- if (fieldProjection.props.format === FormatEnum.MultiSelect && Array.isArray(value)) {
304
- const tagItems = value
305
- .map((id) => {
306
- const option = options.find((o) => o.id === id);
307
- if (option) {
308
- return {
309
- id,
310
- label: option.title,
311
- hue: option.color as any,
312
- };
313
- }
314
- return undefined;
315
- })
316
- .filter((item): item is { id: any; label: string; hue: any } => item !== undefined);
317
-
318
- return createLinks(tagItems);
319
- } else {
320
- const option = options.find((o) => o.id === value);
321
-
322
- if (option) {
323
- const tagItem = {
324
- id: value,
325
- label: option.title,
326
- hue: option.color as any,
327
- };
328
-
329
- return createLinks([tagItem]);
330
- }
331
- }
332
- }
333
-
334
- return '';
335
- }
336
-
337
238
  const value = model.getCellData(cell);
338
239
  return value !== undefined ? String(value) : '';
339
240
  }
@@ -341,8 +242,17 @@ export const TableCellEditor = ({
341
242
 
342
243
  return (
343
244
  <>
344
- <CellValidationMessage validationError={_validationError} __gridScope={__gridScope} />
345
- <GridCellEditor extension={extension} getCellContent={getCellContent} onBlur={handleBlur} />
245
+ <CellValidationMessage validationError={validationError} variant={validationVariant} __gridScope={__gridScope} />
246
+ <GridCellEditor
247
+ extensions={extensions}
248
+ slots={{
249
+ content: {
250
+ className: '!py-(--dx-grid-cell-editor-padding-block)',
251
+ },
252
+ }}
253
+ getCellContent={getCellContent}
254
+ onBlur={handleBlur}
255
+ />
346
256
  </>
347
257
  );
348
258
  };
@@ -3,3 +3,4 @@
3
3
  //
4
4
 
5
5
  export * from './TableCellEditor';
6
+ export { type OnCreateHandler } from './FormCellEditor';
@@ -3,4 +3,3 @@
3
3
  //
4
4
 
5
5
  export * from './Table';
6
- export * from './TableToolbar';
@@ -2,58 +2,69 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { Schema } from 'effect';
5
+ import { Atom } from '@effect-atom/atom-react';
6
+ import * as Schema from 'effect/Schema';
6
7
  import { useEffect } from 'react';
7
8
 
8
- import { Obj, Ref } from '@dxos/echo';
9
- import { FormatEnum, ObjectId, setValue, toJsonSchema, TypedObject, TypeEnum } from '@dxos/echo-schema';
10
- import { faker } from '@dxos/random';
11
- import { live } from '@dxos/react-client/echo';
12
- import { createView, type ViewProjection } from '@dxos/schema';
9
+ import { Format, Type } from '@dxos/echo';
10
+ import { TypeEnum } from '@dxos/echo/internal';
11
+ import { setValue } from '@dxos/effect';
12
+ import { random } from '@dxos/random';
13
+ import { type ProjectionModel } from '@dxos/schema';
13
14
 
14
- import { TableType } from '../types';
15
-
16
- export const TestSchema = TypedObject({ typename: 'example.com/type/Test', version: '0.1.0' })({
17
- id: ObjectId,
15
+ export const TestSchema = Schema.Struct({
18
16
  name: Schema.optional(Schema.String),
19
17
  age: Schema.optional(Schema.Number),
20
18
  active: Schema.optional(Schema.Boolean),
21
19
  netWorth: Schema.optional(Schema.Number),
22
- });
23
-
24
- export const createTable = (schema = TestSchema) => {
25
- return Obj.make(TableType, {
26
- view: Ref.make(
27
- createView({
28
- name: 'Test',
29
- typename: schema.typename,
30
- jsonSchema: toJsonSchema(schema),
31
- }),
32
- ),
33
- });
20
+ }).pipe(
21
+ Type.object({
22
+ typename: 'com.example.type.test',
23
+ version: '0.1.0',
24
+ }),
25
+ );
26
+
27
+ export type TestItem = {
28
+ name: string;
29
+ age: number;
30
+ active: boolean;
31
+ netWorth: number;
34
32
  };
35
33
 
36
- export const createItems = (n: number) => {
37
- const { data } = live({
34
+ /**
35
+ * Creates an Atom containing test items data.
36
+ */
37
+ export const createItemsAtom = (n: number) => {
38
+ return Atom.make<{ data: TestItem[] }>({
38
39
  data: Array.from({ length: n }, () => ({
39
- name: faker.person.fullName(),
40
- age: faker.number.int({ min: 20, max: 70 }),
41
- active: faker.datatype.boolean(),
42
- netWorth: faker.number.int(),
40
+ name: random.person.fullName(),
41
+ age: random.number.int({ min: 20, max: 70 }),
42
+ active: random.datatype.boolean(),
43
+ netWorth: random.number.int(),
43
44
  })),
44
- });
45
+ }).pipe(Atom.keepAlive);
46
+ };
45
47
 
46
- return data;
48
+ /**
49
+ * Creates plain test items (non-reactive).
50
+ */
51
+ export const createItems = (n: number): TestItem[] => {
52
+ return Array.from({ length: n }, () => ({
53
+ name: random.person.fullName(),
54
+ age: random.number.int({ min: 20, max: 70 }),
55
+ active: random.datatype.boolean(),
56
+ netWorth: random.number.int(),
57
+ }));
47
58
  };
48
59
 
49
60
  export type SimulatorProps = {
50
- table: TableType;
61
+ projection: ProjectionModel;
51
62
  items: any[];
52
63
  insertInterval?: number;
53
64
  updateInterval?: number;
54
65
  };
55
66
 
56
- export const useSimulator = ({ items, table, insertInterval, updateInterval }: SimulatorProps) => {
67
+ export const useSimulator = ({ items, projection, insertInterval, updateInterval }: SimulatorProps) => {
57
68
  useEffect(() => {
58
69
  if (!insertInterval) {
59
70
  return;
@@ -74,10 +85,8 @@ export const useSimulator = ({ items, table, insertInterval, updateInterval }: S
74
85
 
75
86
  const i = setInterval(() => {
76
87
  const rowIdx = Math.floor(Math.random() * items.length);
77
- const fields = table.view?.target?.fields ?? [];
88
+ const fields = projection.getFields() ?? [];
78
89
  const columnIdx = Math.floor(Math.random() * fields.length);
79
- // TODO(ZaymonFC): ... This is borked.
80
- const projection: ViewProjection = (table as any)._projection;
81
90
  const field = fields[columnIdx];
82
91
  const item = items[rowIdx];
83
92
 
@@ -105,7 +114,7 @@ export const useSimulator = ({ items, table, insertInterval, updateInterval }: S
105
114
 
106
115
  if (format) {
107
116
  switch (format) {
108
- case FormatEnum.Currency: {
117
+ case Format.TypeFormat.Currency: {
109
118
  item[path] = Math.floor(Math.random() * 1000);
110
119
  break;
111
120
  }
@@ -115,5 +124,5 @@ export const useSimulator = ({ items, table, insertInterval, updateInterval }: S
115
124
  }, updateInterval);
116
125
 
117
126
  return () => clearInterval(i);
118
- }, [items, table.view?.target?.fields, updateInterval]);
127
+ }, [items, projection.getFields(), updateInterval]);
119
128
  };
@@ -2,4 +2,6 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
+ export * from './useAddRow';
6
+ export * from './useProjectionModel';
5
7
  export * from './useTableModel';
@@ -0,0 +1,36 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { useCallback } from 'react';
6
+
7
+ import { type Database, Obj, type Type } from '@dxos/echo';
8
+
9
+ import { type InsertRowResult } from '../model';
10
+
11
+ export type UseAddRowProps = {
12
+ db?: Database.Database;
13
+ schema?: Type.AnyObj;
14
+ };
15
+
16
+ /**
17
+ * Hook that provides a safe row creation function that handles schema validation failures.
18
+ * Returns a callback that attempts to create an object and returns boolean success/failure.
19
+ * Can accept optional data to create objects with specific content (used for committing draft rows).
20
+ */
21
+ export const useAddRow = ({ db, schema }: UseAddRowProps) => {
22
+ return useCallback(
23
+ (data?: any): InsertRowResult => {
24
+ if (db && schema) {
25
+ try {
26
+ db.add(Obj.make(schema, data ?? {}));
27
+ return 'final';
28
+ } catch (error) {
29
+ return 'draft';
30
+ }
31
+ }
32
+ return 'final';
33
+ },
34
+ [db, schema],
35
+ );
36
+ };
@@ -0,0 +1,39 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type Registry } from '@effect-atom/atom-react';
6
+ import { useState } from 'react';
7
+
8
+ import { JsonSchema, Type } from '@dxos/echo';
9
+ import { useAsyncEffect } from '@dxos/react-ui';
10
+ import { ProjectionModel, createEchoChangeCallback } from '@dxos/schema';
11
+
12
+ import { type Table } from '../types';
13
+
14
+ export const useProjectionModel = <S extends Type.AnyEntity>(
15
+ schema: S | undefined,
16
+ table: Table.Table | undefined,
17
+ registry: Registry.Registry,
18
+ ) => {
19
+ const [projection, setProjection] = useState<ProjectionModel | undefined>();
20
+
21
+ useAsyncEffect(async () => {
22
+ if (schema && table) {
23
+ const view = await table.view.load();
24
+ // For mutable schemas (EchoSchema), use the live jsonSchema reference for reactivity.
25
+ // For immutable schemas, create a snapshot.
26
+ const jsonSchema = Type.isMutable(schema) ? schema.jsonSchema : JsonSchema.toJsonSchema(schema);
27
+
28
+ // Always use createEchoChangeCallback since the view is ECHO-backed.
29
+ // Pass schema only when mutable to allow schema mutations.
30
+ const change = createEchoChangeCallback(view, Type.isMutable(schema) ? schema : undefined);
31
+
32
+ const projection = new ProjectionModel({ registry, view, baseSchema: jsonSchema, change });
33
+ projection.normalizeView();
34
+ setProjection(projection);
35
+ }
36
+ }, [schema, table, registry]);
37
+
38
+ return projection;
39
+ };