@dxos/react-ui-table 0.8.4-main.f9ba587 → 0.8.4-main.fcc0d83b33

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 (185) 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 +1158 -2872
  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 +1158 -2872
  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 +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 +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 -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 +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 +116 -34
  78. package/dist/types/src/model/table-model.d.ts.map +1 -1
  79. package/dist/types/src/model/table-presentation.d.ts +5 -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/index.d.ts +2 -0
  85. package/dist/types/src/testing/index.d.ts.map +1 -0
  86. package/dist/types/src/testing/useTestTableModel.d.ts +25 -0
  87. package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -0
  88. package/dist/types/src/translations.d.ts +21 -23
  89. package/dist/types/src/translations.d.ts.map +1 -1
  90. package/dist/types/src/types/Table.d.ts +26 -0
  91. package/dist/types/src/types/Table.d.ts.map +1 -0
  92. package/dist/types/src/types/index.d.ts +1 -1
  93. package/dist/types/src/types/index.d.ts.map +1 -1
  94. package/dist/types/src/util/dynamic-table.d.ts +15 -14
  95. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  96. package/dist/types/src/util/dynamic-table.test.d.ts +2 -0
  97. package/dist/types/src/util/dynamic-table.test.d.ts.map +1 -0
  98. package/dist/types/src/util/index.d.ts +2 -1
  99. package/dist/types/src/util/index.d.ts.map +1 -1
  100. package/dist/types/src/util/query-ast.d.ts +7 -0
  101. package/dist/types/src/util/query-ast.d.ts.map +1 -0
  102. package/dist/types/src/util/schema.d.ts +1 -1
  103. package/dist/types/src/util/schema.d.ts.map +1 -1
  104. package/dist/types/src/util/signals.d.ts.map +1 -1
  105. package/dist/types/src/util/sort.d.ts +12 -0
  106. package/dist/types/src/util/sort.d.ts.map +1 -0
  107. package/dist/types/src/util/table-buttons.d.ts +20 -20
  108. package/dist/types/src/util/table-buttons.d.ts.map +1 -1
  109. package/dist/types/src/util/table-controls.d.ts +4 -4
  110. package/dist/types/src/util/table-controls.d.ts.map +1 -1
  111. package/dist/types/src/util/tag.d.ts.map +1 -1
  112. package/dist/types/tsconfig.tsbuildinfo +1 -1
  113. package/package.json +80 -69
  114. package/src/components/Table/ColumnActionsMenu.tsx +18 -13
  115. package/src/components/Table/ColumnSettings.tsx +18 -15
  116. package/src/components/Table/DynamicTable.stories.tsx +40 -36
  117. package/src/components/Table/DynamicTable.tsx +38 -34
  118. package/src/components/Table/Relations.stories.tsx +281 -0
  119. package/src/components/Table/RowActionsMenu.tsx +28 -18
  120. package/src/components/Table/Table.stories.tsx +198 -220
  121. package/src/components/Table/Table.tsx +47 -376
  122. package/src/components/Table/TableContent.tsx +421 -0
  123. package/src/components/Table/TableToolbar.tsx +111 -0
  124. package/src/components/TableCellEditor/CellValidationMessage.tsx +6 -6
  125. package/src/components/TableCellEditor/FormCellEditor.tsx +153 -49
  126. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +38 -77
  127. package/src/components/TableCellEditor/TableCellEditor.tsx +104 -233
  128. package/src/components/TableCellEditor/index.ts +1 -0
  129. package/src/components/index.ts +0 -1
  130. package/src/components/testing.ts +46 -37
  131. package/src/hooks/index.ts +2 -1
  132. package/src/hooks/useAddRow.ts +14 -14
  133. package/src/hooks/useProjectionModel.ts +39 -0
  134. package/src/hooks/useTableModel.ts +41 -35
  135. package/src/index.ts +0 -5
  136. package/src/model/modal-controller.ts +24 -41
  137. package/src/model/selection-model.ts +75 -41
  138. package/src/model/table-model.test.ts +91 -56
  139. package/src/model/table-model.ts +468 -187
  140. package/src/model/table-presentation.test.ts +77 -60
  141. package/src/model/table-presentation.ts +112 -66
  142. package/src/playwright/TableManager.ts +13 -6
  143. package/src/playwright/playwright.config.ts +1 -1
  144. package/src/playwright/smoke.spec.ts +180 -43
  145. package/src/testing/index.ts +5 -0
  146. package/src/testing/useTestTableModel.ts +133 -0
  147. package/src/translations.ts +20 -22
  148. package/src/types/Table.ts +79 -0
  149. package/src/types/index.ts +2 -2
  150. package/src/util/dynamic-table.test.ts +77 -0
  151. package/src/util/dynamic-table.ts +51 -36
  152. package/src/util/index.ts +2 -1
  153. package/src/util/query-ast.ts +22 -0
  154. package/src/util/schema.ts +4 -4
  155. package/src/util/sort.ts +90 -0
  156. package/src/util/table-buttons.ts +4 -3
  157. package/src/util/table-controls.ts +6 -3
  158. package/dist/lib/browser/chunk-Q5QQXJUO.mjs +0 -27
  159. package/dist/lib/browser/chunk-Q5QQXJUO.mjs.map +0 -7
  160. package/dist/lib/node-esm/chunk-ZNOU6LHG.mjs +0 -29
  161. package/dist/lib/node-esm/chunk-ZNOU6LHG.mjs.map +0 -7
  162. package/dist/types/src/components/Table/CreateRefPanel.d.ts +0 -9
  163. package/dist/types/src/components/Table/CreateRefPanel.d.ts.map +0 -1
  164. package/dist/types/src/components/Table/relations.stories.d.ts +0 -9
  165. package/dist/types/src/components/Table/relations.stories.d.ts.map +0 -1
  166. package/dist/types/src/components/TableCellEditor/extension.d.ts +0 -13
  167. package/dist/types/src/components/TableCellEditor/extension.d.ts.map +0 -1
  168. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +0 -21
  169. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +0 -1
  170. package/dist/types/src/components/TableToolbar/index.d.ts +0 -2
  171. package/dist/types/src/components/TableToolbar/index.d.ts.map +0 -1
  172. package/dist/types/src/model/table-sorting.d.ts +0 -53
  173. package/dist/types/src/model/table-sorting.d.ts.map +0 -1
  174. package/dist/types/src/types/table.d.ts +0 -56
  175. package/dist/types/src/types/table.d.ts.map +0 -1
  176. package/dist/types/src/util/table.d.ts +0 -14
  177. package/dist/types/src/util/table.d.ts.map +0 -1
  178. package/src/components/Table/CreateRefPanel.tsx +0 -84
  179. package/src/components/Table/relations.stories.tsx +0 -123
  180. package/src/components/TableCellEditor/extension.ts +0 -61
  181. package/src/components/TableToolbar/TableToolbar.tsx +0 -102
  182. package/src/components/TableToolbar/index.ts +0 -5
  183. package/src/model/table-sorting.ts +0 -216
  184. package/src/types/table.ts +0 -27
  185. 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';
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/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 { tagPicker, 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,44 +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
- const editorSlots = { scroller: { className: '!plb-[--dx-grid-cell-editor-padding-block]' } };
96
-
97
93
  export const TableCellEditor = ({
94
+ __gridScope,
98
95
  model,
99
96
  modals,
100
- onEnter,
101
97
  onFocus,
102
- onQuery,
103
- __gridScope,
98
+ onSave,
104
99
  }: GridScopedProps<TableCellEditorProps>) => {
105
- const { id: gridId, editing, setEditing } = useGridContext('TableCellEditor', __gridScope);
100
+ const { editing, setEditing } = useGridContext('TableCellEditor', __gridScope);
106
101
  const suppressNextBlur = useRef(false);
107
- const { themeMode } = useThemeContext();
108
- const [_validationError, setValidationError] = useState<string | null>(null);
109
- const [_validationVariant, setValidationVariant] = useState<'error' | 'warning'>('error');
102
+ const [validationError, setValidationError] = useState<string | null>(null);
103
+ const [validationVariant, setValidationVariant] = useState<'error' | 'warning'>('error');
110
104
 
111
105
  const fieldProjection = useMemo<FieldProjection | undefined>(() => {
112
106
  if (!model || !editing) {
@@ -114,44 +108,13 @@ export const TableCellEditor = ({
114
108
  }
115
109
 
116
110
  const { col } = parseCellIndex(editing.index);
117
- const field = model.projection.view.fields[col];
111
+ const field = model.projection.getFields()[col];
118
112
  const fieldProjection = model.projection.getFieldProjection(field.id);
119
113
  invariant(fieldProjection);
120
114
  return fieldProjection;
121
115
  }, [model, editing]);
122
116
 
123
- useEffect(() => {
124
- // Check for existing validation errors when editing starts (for draft rows).
125
- if (!model || !editing || !fieldProjection) {
126
- setValidationError(null);
127
- return;
128
- }
129
-
130
- const cell = parseCellIndex(editing.index);
131
- const { row, col } = cell;
132
-
133
- if (model.isDraftCell(cell)) {
134
- const field = model.projection.view.fields[col];
135
- const hasValidationError = model.hasDraftRowValidationError(row, field.path);
136
-
137
- if (hasValidationError) {
138
- const draftRows = model.draftRows.value;
139
- if (row >= 0 && row < draftRows.length) {
140
- const draftRow = draftRows[row];
141
- const validationError = draftRow.validationErrors?.find((error) => error.path === field.path);
142
- if (validationError) {
143
- setValidationError(validationError.message);
144
- setValidationVariant('warning');
145
- }
146
- }
147
- } else {
148
- setValidationError(null);
149
- }
150
- } else {
151
- setValidationError(null);
152
- }
153
- }, [model, editing, fieldProjection]);
154
-
117
+ // TOOD(burdon): Attach to event handler?
155
118
  const handleEnter = useCallback(
156
119
  async (value: any) => {
157
120
  if (!model || !editing) {
@@ -164,27 +127,34 @@ export const TableCellEditor = ({
164
127
  if (validationResult.valid) {
165
128
  setValidationError(null);
166
129
  model.setCellData(cell, value);
167
- onEnter?.(cell);
168
130
  onFocus?.();
169
131
  setEditing(null);
132
+ onSave?.();
170
133
  } else {
171
134
  setValidationError(validationResult.error);
172
135
  setValidationVariant('error');
173
136
  }
174
137
  },
175
- [model, editing, onEnter, onFocus, setEditing],
138
+ [model, editing, onFocus, setEditing],
176
139
  );
177
140
 
178
141
  const handleBlur = useCallback<EditorBlurHandler>(
179
142
  async (value) => {
180
- if (!model || !editing) {
181
- return;
182
- }
183
143
  if (suppressNextBlur.current) {
184
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
+ }
185
157
  }
186
-
187
- // Don't save on blur - let handleClose handle validation and saving
188
158
  },
189
159
  [model, editing],
190
160
  );
@@ -196,179 +166,75 @@ export const TableCellEditor = ({
196
166
  }
197
167
 
198
168
  const cell = parseCellIndex(editing.index);
199
-
200
169
  if (value !== undefined) {
201
170
  // Pre-commit validation check.
202
171
  const result = await model.validateCellData(cell, value);
203
172
 
204
173
  if (result.valid) {
174
+ suppressNextBlur.current = true;
205
175
  setValidationError(null);
206
176
  model.setCellData(cell, value);
207
177
  setEditing(null);
208
- onEnter?.(cell);
178
+ onSave?.();
209
179
  if (event && onFocus) {
210
- onFocus(determineNavigationAxis(event), determineNavigationDelta(event));
180
+ onFocus(determineNavigationAxis(event), determineNavigationDelta(event), cell);
211
181
  }
212
182
  } else {
213
183
  setValidationError(result.error);
214
184
  setValidationVariant('error');
215
185
  }
216
186
  } else {
187
+ suppressNextBlur.current = true;
217
188
  setValidationError(null);
218
189
  setEditing(null);
190
+ onSave?.();
219
191
  if (event && onFocus) {
220
192
  onFocus(determineNavigationAxis(event), determineNavigationDelta(event));
221
193
  }
222
194
  }
223
195
  },
224
- [model, editing, onFocus, onEnter, fieldProjection, setEditing],
196
+ [model, editing, onFocus, fieldProjection, setEditing, onSave],
225
197
  );
226
198
 
227
- const extension = useMemo(() => {
199
+ const extensions = useMemo(() => {
228
200
  if (!fieldProjection) {
229
201
  return [];
230
202
  }
231
203
 
232
- const extension = [
204
+ const extensions = [
233
205
  editorKeys({
234
206
  onClose: handleClose,
235
207
  ...(editing?.initialContent && { onNav: handleClose }),
236
208
  }),
237
209
  ];
238
-
239
- const format = fieldProjection.props.format;
240
-
241
- if (format === FormatEnum.SingleSelect || format === FormatEnum.MultiSelect) {
242
- // TODO(ZaymonFC): Reconcile this with the TagPicker component?
243
- // Add markdown extensions needed by tag picker.
244
- extension.push(createMarkdownExtensions({ themeMode }));
245
-
246
- const options = fieldProjection.props.options || [];
247
-
248
- const mode = format === FormatEnum.SingleSelect ? ('single-select' as const) : ('multi-select' as const);
249
-
250
- extension.push(
251
- tagPicker({
252
- mode,
253
- keymap: false,
254
- onSearch: (text, selectedIds) => {
255
- return options
256
- .filter(
257
- (option) =>
258
- selectedIds.indexOf(option.id) === -1 &&
259
- (text.length === 0 || option.title.toLowerCase().includes(text.toLowerCase())),
260
- )
261
- .map((option) => ({
262
- id: option.id,
263
- label: option.title,
264
- hue: option.color as any,
265
- }));
266
- },
267
- onUpdate: (ids) => {
268
- if (model && editing) {
269
- if (ids.length === 0) {
270
- return;
271
- }
272
- if (mode === 'single-select') {
273
- 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);
274
222
  } else {
275
- void handleEnter(ids);
223
+ setValidationError(result.error);
224
+ setValidationVariant('error');
276
225
  }
277
- }
278
- },
279
- }),
226
+ });
227
+ }, 10),
228
+ ),
280
229
  );
281
230
  }
282
231
 
283
- if (onQuery) {
284
- switch (fieldProjection.props.format) {
285
- case FormatEnum.Ref: {
286
- extension.push([
287
- completion({
288
- onQuery: (text) => onQuery(fieldProjection, text),
289
- onMatch: (data) => {
290
- if (model && editing && modals) {
291
- if (isCreateOption(data)) {
292
- const { field, props } = fieldProjection;
293
- if (props.referenceSchema) {
294
- suppressNextBlur.current = true;
295
- modals.openCreateRef(
296
- props.referenceSchema,
297
- document.querySelector(cellQuery(editing.index, gridId)),
298
- {
299
- [field.referencePath!]: data.text,
300
- },
301
- (data) => {
302
- void handleEnter(data);
303
- },
304
- );
305
- }
306
- } else {
307
- void handleEnter(data);
308
- }
309
- }
310
- },
311
- }),
312
- ]);
313
- break;
314
- }
315
- }
316
- }
317
-
318
- return extension;
319
- }, [model, modals, editing, fieldProjection, handleClose, themeMode]);
232
+ return extensions;
233
+ }, [model, modals, editing, fieldProjection, handleClose]);
320
234
 
321
235
  const getCellContent = useCallback<GridCellEditorProps['getCellContent']>(() => {
322
236
  if (model && editing) {
323
237
  const cell = parseCellIndex(editing.index);
324
- const { col } = cell;
325
- const field = model.projection.view.fields[col];
326
- const fieldProjection = model.projection.getFieldProjection(field.id);
327
-
328
- if (
329
- fieldProjection?.props.format === FormatEnum.SingleSelect ||
330
- fieldProjection?.props.format === FormatEnum.MultiSelect
331
- ) {
332
- const value = model.getCellData(cell);
333
-
334
- if (value !== undefined) {
335
- const options = fieldProjection.props.options || [];
336
-
337
- if (fieldProjection.props.format === FormatEnum.MultiSelect) {
338
- const tagItems = value
339
- .split(',')
340
- .map((id: string) => {
341
- const option = options.find((o) => o.id === id);
342
- if (option) {
343
- return {
344
- id,
345
- label: option.title,
346
- hue: option.color as any,
347
- };
348
- }
349
- return undefined;
350
- })
351
- .filter((item: any): item is { id: any; label: string; hue: any } => item !== undefined);
352
-
353
- return createLinks(tagItems);
354
- } else {
355
- const option = options.find((o) => o.id === value);
356
-
357
- if (option) {
358
- const tagItem = {
359
- id: value,
360
- label: option.title,
361
- hue: option.color as any,
362
- };
363
-
364
- return createLinks([tagItem]);
365
- }
366
- }
367
- }
368
-
369
- return '';
370
- }
371
-
372
238
  const value = model.getCellData(cell);
373
239
  return value !== undefined ? String(value) : '';
374
240
  }
@@ -376,12 +242,17 @@ export const TableCellEditor = ({
376
242
 
377
243
  return (
378
244
  <>
379
- <CellValidationMessage
380
- validationError={_validationError}
381
- variant={_validationVariant}
382
- __gridScope={__gridScope}
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}
383
255
  />
384
- <GridCellEditor extension={extension} getCellContent={getCellContent} onBlur={handleBlur} slots={editorSlots} />
385
256
  </>
386
257
  );
387
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,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
  };