@dxos/react-ui-table 0.8.4-main.fd6878d → 0.8.4-staging.60fe92afc8

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