@dxos/react-ui-table 0.8.4-main.67995b8 → 0.8.4-main.70d3990

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 (143) hide show
  1. package/dist/lib/browser/chunk-5XWSB3JP.mjs +71 -0
  2. package/dist/lib/browser/chunk-5XWSB3JP.mjs.map +7 -0
  3. package/dist/lib/browser/{chunk-5XLSHLOG.mjs → chunk-KNC7XYT6.mjs} +573 -541
  4. package/dist/lib/browser/chunk-KNC7XYT6.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +625 -698
  6. package/dist/lib/browser/index.mjs.map +4 -4
  7. package/dist/lib/browser/meta.json +1 -1
  8. package/dist/lib/browser/testing/index.mjs +16 -24
  9. package/dist/lib/browser/testing/index.mjs.map +3 -3
  10. package/dist/lib/browser/types/index.mjs +3 -3
  11. package/dist/lib/node-esm/{chunk-Y66YLSTC.mjs → chunk-CWSGS3Y2.mjs} +573 -541
  12. package/dist/lib/node-esm/chunk-CWSGS3Y2.mjs.map +7 -0
  13. package/dist/lib/node-esm/chunk-MKTMOY46.mjs +72 -0
  14. package/dist/lib/node-esm/chunk-MKTMOY46.mjs.map +7 -0
  15. package/dist/lib/node-esm/index.mjs +625 -698
  16. package/dist/lib/node-esm/index.mjs.map +4 -4
  17. package/dist/lib/node-esm/meta.json +1 -1
  18. package/dist/lib/node-esm/testing/index.mjs +16 -24
  19. package/dist/lib/node-esm/testing/index.mjs.map +3 -3
  20. package/dist/lib/node-esm/types/index.mjs +3 -3
  21. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts +1 -1
  22. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts.map +1 -1
  23. package/dist/types/src/components/Table/ColumnSettings.d.ts +1 -1
  24. package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
  25. package/dist/types/src/components/Table/DynamicTable.d.ts +2 -2
  26. package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
  27. package/dist/types/src/components/Table/DynamicTable.stories.d.ts +51 -3
  28. package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
  29. package/dist/types/src/components/Table/Relations.stories.d.ts +108 -0
  30. package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -0
  31. package/dist/types/src/components/Table/RowActionsMenu.d.ts +1 -1
  32. package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
  33. package/dist/types/src/components/Table/Table.d.ts +8 -4
  34. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  35. package/dist/types/src/components/Table/Table.stories.d.ts +105 -4
  36. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  37. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts.map +1 -1
  38. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +12 -6
  39. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
  40. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +12 -17
  41. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  42. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +39 -4
  43. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  44. package/dist/types/src/components/TableCellEditor/index.d.ts +1 -0
  45. package/dist/types/src/components/TableCellEditor/index.d.ts.map +1 -1
  46. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +2 -2
  47. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -1
  48. package/dist/types/src/components/testing.d.ts +3 -5
  49. package/dist/types/src/components/testing.d.ts.map +1 -1
  50. package/dist/types/src/hooks/index.d.ts +2 -1
  51. package/dist/types/src/hooks/index.d.ts.map +1 -1
  52. package/dist/types/src/hooks/useAddRow.d.ts +3 -2
  53. package/dist/types/src/hooks/useAddRow.d.ts.map +1 -1
  54. package/dist/types/src/hooks/useProjectionModel.d.ts +5 -0
  55. package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -0
  56. package/dist/types/src/hooks/useTableModel.d.ts +5 -6
  57. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  58. package/dist/types/src/model/modal-controller.d.ts +0 -7
  59. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  60. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  61. package/dist/types/src/model/table-model.d.ts +14 -14
  62. package/dist/types/src/model/table-model.d.ts.map +1 -1
  63. package/dist/types/src/model/table-presentation.d.ts +2 -1
  64. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  65. package/dist/types/src/model/table-sorting.d.ts +9 -6
  66. package/dist/types/src/model/table-sorting.d.ts.map +1 -1
  67. package/dist/types/src/playwright/TableManager.d.ts +1 -1
  68. package/dist/types/src/playwright/TableManager.d.ts.map +1 -1
  69. package/dist/types/src/testing/useTestTableModel.d.ts +21 -34
  70. package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -1
  71. package/dist/types/src/translations.d.ts +2 -2
  72. package/dist/types/src/types/Table.d.ts +28 -0
  73. package/dist/types/src/types/Table.d.ts.map +1 -0
  74. package/dist/types/src/types/index.d.ts +1 -1
  75. package/dist/types/src/types/index.d.ts.map +1 -1
  76. package/dist/types/src/util/dynamic-table.d.ts +9 -9
  77. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  78. package/dist/types/src/util/index.d.ts +0 -1
  79. package/dist/types/src/util/index.d.ts.map +1 -1
  80. package/dist/types/src/util/schema.d.ts +1 -1
  81. package/dist/types/src/util/schema.d.ts.map +1 -1
  82. package/dist/types/src/util/table-buttons.d.ts.map +1 -1
  83. package/dist/types/src/util/table-controls.d.ts.map +1 -1
  84. package/dist/types/tsconfig.tsbuildinfo +1 -1
  85. package/package.json +61 -59
  86. package/src/components/Table/ColumnActionsMenu.tsx +1 -1
  87. package/src/components/Table/ColumnSettings.tsx +1 -1
  88. package/src/components/Table/DynamicTable.stories.tsx +21 -17
  89. package/src/components/Table/DynamicTable.tsx +19 -13
  90. package/src/components/Table/Relations.stories.tsx +237 -0
  91. package/src/components/Table/RowActionsMenu.tsx +23 -15
  92. package/src/components/Table/Table.stories.tsx +103 -86
  93. package/src/components/Table/Table.tsx +140 -115
  94. package/src/components/TableCellEditor/CellValidationMessage.tsx +1 -1
  95. package/src/components/TableCellEditor/FormCellEditor.tsx +153 -42
  96. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +28 -66
  97. package/src/components/TableCellEditor/TableCellEditor.tsx +75 -225
  98. package/src/components/TableCellEditor/index.ts +1 -0
  99. package/src/components/TableToolbar/TableToolbar.tsx +34 -24
  100. package/src/components/testing.ts +4 -4
  101. package/src/hooks/index.ts +2 -1
  102. package/src/hooks/useAddRow.ts +7 -5
  103. package/src/hooks/useProjectionModel.ts +19 -0
  104. package/src/hooks/useTableModel.ts +12 -14
  105. package/src/model/modal-controller.ts +1 -29
  106. package/src/model/selection-model.ts +3 -2
  107. package/src/model/table-model.test.ts +21 -12
  108. package/src/model/table-model.ts +62 -56
  109. package/src/model/table-presentation.test.ts +25 -23
  110. package/src/model/table-presentation.ts +82 -41
  111. package/src/model/table-sorting.ts +32 -14
  112. package/src/playwright/TableManager.ts +3 -3
  113. package/src/playwright/smoke.spec.ts +132 -38
  114. package/src/testing/useTestTableModel.ts +34 -22
  115. package/src/translations.ts +2 -2
  116. package/src/types/Table.ts +89 -0
  117. package/src/types/index.ts +1 -1
  118. package/src/util/dynamic-table.ts +20 -24
  119. package/src/util/index.ts +0 -1
  120. package/src/util/schema.ts +4 -4
  121. package/src/util/table-buttons.ts +4 -3
  122. package/src/util/table-controls.ts +4 -1
  123. package/dist/lib/browser/chunk-5XLSHLOG.mjs.map +0 -7
  124. package/dist/lib/browser/chunk-L2LSB6KV.mjs +0 -18
  125. package/dist/lib/browser/chunk-L2LSB6KV.mjs.map +0 -7
  126. package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs +0 -20
  127. package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs.map +0 -7
  128. package/dist/lib/node-esm/chunk-Y66YLSTC.mjs.map +0 -7
  129. package/dist/types/src/components/Table/CreateRefPanel.d.ts +0 -9
  130. package/dist/types/src/components/Table/CreateRefPanel.d.ts.map +0 -1
  131. package/dist/types/src/components/Table/relations.stories.d.ts +0 -9
  132. package/dist/types/src/components/Table/relations.stories.d.ts.map +0 -1
  133. package/dist/types/src/components/TableCellEditor/extension.d.ts +0 -13
  134. package/dist/types/src/components/TableCellEditor/extension.d.ts.map +0 -1
  135. package/dist/types/src/types/schema.d.ts +0 -8
  136. package/dist/types/src/types/schema.d.ts.map +0 -1
  137. package/dist/types/src/util/table.d.ts +0 -11
  138. package/dist/types/src/util/table.d.ts.map +0 -1
  139. package/src/components/Table/CreateRefPanel.tsx +0 -86
  140. package/src/components/Table/relations.stories.tsx +0 -116
  141. package/src/components/TableCellEditor/extension.ts +0 -61
  142. package/src/types/schema.ts +0 -16
  143. package/src/util/table.ts +0 -49
@@ -2,49 +2,47 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { type Schema } from 'effect/Schema';
5
+ import type * as Schema from 'effect/Schema';
6
+ import * as String from 'effect/String';
6
7
  import React, {
7
8
  type MouseEvent,
8
9
  type PropsWithChildren,
9
10
  type WheelEvent,
10
11
  forwardRef,
11
12
  useCallback,
13
+ useEffect,
12
14
  useImperativeHandle,
13
- useState,
14
15
  useMemo,
15
- useEffect,
16
+ useState,
16
17
  } from 'react';
17
18
 
18
- import { type Client } from '@dxos/client';
19
- import { Filter } from '@dxos/echo';
20
- import { getValue } from '@dxos/echo-schema';
21
- import { invariant } from '@dxos/invariant';
22
19
  // TODO(wittjosiah): Remove dependency on react-client.
23
- import { getSpace } from '@dxos/react-client/echo';
24
- import { useTranslation } from '@dxos/react-ui';
20
+ import { type Client } from '@dxos/react-client';
25
21
  import { useAttention } from '@dxos/react-ui-attention';
26
22
  import {
27
- closestCell,
28
23
  type DxGridElement,
29
- type DxGridPosition,
30
- type GridContentProps,
31
- Grid,
32
24
  type DxGridPlane,
33
25
  type DxGridPlaneRange,
34
- gridSeparatorInlineEnd,
26
+ type DxGridPosition,
27
+ Grid,
28
+ type GridContentProps,
29
+ closestCell,
35
30
  gridSeparatorBlockEnd,
31
+ gridSeparatorInlineEnd,
36
32
  } from '@dxos/react-ui-grid';
33
+ import { DxEditRequest } from '@dxos/react-ui-grid';
37
34
  import { mx } from '@dxos/react-ui-theme';
38
- import { isNotFalsy, safeParseInt } from '@dxos/util';
35
+
36
+ import { type InsertRowResult, ModalController, type TableModel, type TablePresentation } from '../../model';
37
+ import { tableButtons, tableControls } from '../../util';
38
+ import { type OnCreateHandler, type TableCellEditorProps, TableValueEditor } from '../TableCellEditor';
39
39
 
40
40
  import { ColumnActionsMenu } from './ColumnActionsMenu';
41
41
  import { ColumnSettings } from './ColumnSettings';
42
- import { CreateRefPanel } from './CreateRefPanel';
43
42
  import { RowActionsMenu } from './RowActionsMenu';
44
- import { ModalController, type TableModel, type TablePresentation } from '../../model';
45
- import { translationKey } from '../../translations';
46
- import { tableButtons, tableControls } from '../../util';
47
- import { createOption, TableValueEditor, type TableCellEditorProps } from '../TableCellEditor';
43
+
44
+ const columnDefault = { grid: { minSize: 80, maxSize: 640 } };
45
+ const rowDefault = { frozenRowsStart: { readonly: true, focusUnfurl: false } };
48
46
 
49
47
  //
50
48
  // Table.Root
@@ -76,27 +74,34 @@ const TableRoot = ({ children, role = 'article' }: TableRootProps) => {
76
74
 
77
75
  export type TableController = {
78
76
  update?: (cell?: DxGridPosition) => void;
77
+ handleInsertRowResult?: (insertRowResult?: InsertRowResult) => void;
79
78
  };
80
79
 
81
80
  export type TableMainProps = {
82
81
  model?: TableModel;
83
82
  presentation?: TablePresentation;
84
- schema?: Schema.AnyNoContext;
83
+ schema?: Schema.Schema.AnyNoContext;
85
84
  client?: Client;
86
85
  // TODO(burdon): Rename since attention isn't a useful concept here? Standardize across other components. Pass property into useAttention.
87
86
  ignoreAttention?: boolean;
87
+ onCreate?: OnCreateHandler;
88
88
  onRowClick?: (row: any) => void;
89
+ testId?: string;
89
90
  };
90
91
 
91
92
  const TableMain = forwardRef<TableController, TableMainProps>(
92
- ({ model, presentation, ignoreAttention, schema, client, onRowClick }, forwardedRef) => {
93
+ ({ model, presentation, ignoreAttention, schema, client, onCreate, onRowClick, testId }, forwardedRef) => {
93
94
  const [dxGrid, setDxGrid] = useState<DxGridElement | null>(null);
94
95
  const { hasAttention } = useAttention(model?.id ?? 'table');
95
- const { t } = useTranslation(translationKey);
96
96
  const modals = useMemo(() => new ModalController(), []);
97
97
 
98
98
  const draftRowCount = model?.getDraftRowCount() ?? 0;
99
99
 
100
+ const handleSave = useCallback(() => {
101
+ dxGrid?.updateCells(true);
102
+ dxGrid?.requestUpdate();
103
+ }, [dxGrid]);
104
+
100
105
  const frozen = useMemo(() => {
101
106
  const noActionColumn =
102
107
  model?.features.dataEditable === false &&
@@ -105,7 +110,7 @@ const TableMain = forwardRef<TableController, TableMainProps>(
105
110
 
106
111
  return {
107
112
  frozenRowsStart: 1,
108
- frozenRowsEnd: draftRowCount,
113
+ frozenRowsEnd: Math.max(1, draftRowCount),
109
114
  frozenColsStart: model?.features.selection.enabled ? 1 : 0,
110
115
  frozenColsEnd: noActionColumn ? 0 : 1,
111
116
  };
@@ -123,6 +128,23 @@ const TableMain = forwardRef<TableController, TableMainProps>(
123
128
  dxGrid.getCells = getCells;
124
129
  }, [presentation, dxGrid, getCells]);
125
130
 
131
+ const handleInsertRowResult = useCallback(
132
+ (insertResult?: InsertRowResult) => {
133
+ if (insertResult === 'draft') {
134
+ requestAnimationFrame(() => {
135
+ dxGrid?.setFocus({ plane: 'frozenRowsEnd', col: 0, row: 0 });
136
+ dxGrid?.refocus();
137
+ });
138
+ } else {
139
+ requestAnimationFrame(() => {
140
+ dxGrid?.setFocus({ plane: 'grid', col: 0, row: model ? model.getRowCount() - 1 : 0 });
141
+ dxGrid?.refocus();
142
+ });
143
+ }
144
+ },
145
+ [model, dxGrid],
146
+ );
147
+
126
148
  /**
127
149
  * Provides an external controller that can be called to repaint the table.
128
150
  */
@@ -140,16 +162,36 @@ const TableMain = forwardRef<TableController, TableMainProps>(
140
162
  dxGrid.requestUpdate();
141
163
  }
142
164
  },
165
+ handleInsertRowResult,
143
166
  };
144
- }, [presentation, dxGrid]);
167
+ }, [presentation, dxGrid, model]);
168
+
169
+ const handleSaveDraftRow = useCallback(
170
+ (rowIndex = 0) => {
171
+ if (model && dxGrid) {
172
+ const didCommitSuccessfully = model.commitDraftRow(rowIndex);
173
+ if (didCommitSuccessfully) {
174
+ requestAnimationFrame(() => {
175
+ dxGrid.scrollToEndRow();
176
+ });
177
+ }
178
+ }
179
+ },
180
+ [model, dxGrid],
181
+ );
145
182
 
146
183
  const handleGridClick = useCallback(
147
184
  (event: MouseEvent) => {
148
- const rowIndex = safeParseInt((event.target as HTMLElement).closest('[aria-rowindex]')?.ariaRowIndex ?? '');
149
- if (rowIndex != null) {
185
+ const cell = closestCell(event.target as HTMLElement);
186
+ if (cell) {
187
+ const { row: rowIndex, plane } = cell;
150
188
  if (onRowClick) {
151
- const row = model?.getRowAt(rowIndex);
152
- row && onRowClick(row);
189
+ if (plane === 'grid') {
190
+ const row = model?.getRowAt(rowIndex);
191
+ row && onRowClick(row);
192
+ } else {
193
+ onRowClick(cell);
194
+ }
153
195
  }
154
196
 
155
197
  if (model?.features.selection.enabled && model?.selection.selectionMode === 'single') {
@@ -192,14 +234,7 @@ const TableMain = forwardRef<TableController, TableMainProps>(
192
234
  break;
193
235
  }
194
236
  case 'saveDraftRow': {
195
- if (model) {
196
- const didCommitSuccessfully = model.commitDraftRow(data.rowIndex);
197
- if (dxGrid && didCommitSuccessfully) {
198
- requestAnimationFrame(() => {
199
- dxGrid.scrollToEndRow();
200
- });
201
- }
202
- }
237
+ handleSaveDraftRow();
203
238
  break;
204
239
  }
205
240
  }
@@ -240,32 +275,19 @@ const TableMain = forwardRef<TableController, TableMainProps>(
240
275
  );
241
276
 
242
277
  const handleFocus = useCallback<NonNullable<TableCellEditorProps['onFocus']>>(
243
- (increment, delta) => {
244
- if (dxGrid) {
245
- dxGrid.refocus(increment, delta);
246
- }
247
- },
248
- [dxGrid],
249
- );
250
-
251
- const handleEnter = useCallback<NonNullable<TableCellEditorProps['onEnter']>>(
252
- (cell) => {
253
- if (!model?.features.dataEditable) {
254
- return;
255
- }
256
-
257
- // TODO(burdon): Insert row only if bottom row isn't completely blank already.
258
- if (model && cell.row === model.getRowCount() - 1) {
259
- model.insertRow();
260
- if (dxGrid) {
261
- requestAnimationFrame(() => {
262
- dxGrid?.scrollToRow(cell.row + 1);
263
- dxGrid?.refocus('row', 1);
264
- });
278
+ (increment, delta, cell) => {
279
+ if (dxGrid && model) {
280
+ if (cell?.plane === 'grid' && cell?.row >= model.getRowCount() - 1 && increment !== 'col') {
281
+ handleInsertRowResult(draftRowCount < 1 ? model.insertRow() : 'final');
282
+ } else if (cell?.plane === 'frozenRowsEnd' && increment === 'row') {
283
+ handleSaveDraftRow(cell.row);
284
+ handleInsertRowResult(model.insertRow());
285
+ } else {
286
+ dxGrid.refocus(increment, delta);
265
287
  }
266
288
  }
267
289
  },
268
- [model, dxGrid],
290
+ [dxGrid, model],
269
291
  );
270
292
 
271
293
  const handleKeyDown = useCallback<NonNullable<GridContentProps['onKeyDown']>>(
@@ -279,12 +301,64 @@ const TableMain = forwardRef<TableController, TableMainProps>(
279
301
  return;
280
302
  }
281
303
 
304
+ // Handle Meta+C (Copy) and Meta+V (Paste) commands
305
+ if (event.metaKey || event.ctrlKey) {
306
+ switch (event.key) {
307
+ case 'c': {
308
+ // Copy focused cell's text content to clipboard
309
+ try {
310
+ const cellData = model.getCellData(cell);
311
+ const textContent = cellData?.toString() ?? '';
312
+ void navigator.clipboard.writeText(textContent);
313
+ event.preventDefault();
314
+ } catch (error) {
315
+ console.warn('Failed to copy cell content:', error);
316
+ }
317
+ break;
318
+ }
319
+ case 'v': {
320
+ // Paste clipboard content to focused cell
321
+ event.preventDefault();
322
+ void navigator.clipboard.readText().then((clipboardText) => {
323
+ try {
324
+ // Attempt to set the cell's content to clipboard content
325
+ model.setCellData(cell, String.trim(clipboardText).replace(/[\n\r]+/, ' '));
326
+ handleSave();
327
+ } catch {
328
+ // If validation fails, emit a DxEditRequest event with initialContent from clipboard
329
+ // TODO(thure): Should `dx-grid` expose a method like this?
330
+ const cellElement = (event.target as HTMLElement).closest(
331
+ '[data-dx-grid-action="cell"]',
332
+ ) as HTMLElement;
333
+ if (cellElement) {
334
+ const rect = cellElement.getBoundingClientRect();
335
+ const editRequest = new DxEditRequest({
336
+ cellIndex: `${cell.plane},${cell.col},${cell.row}`,
337
+ cellBox: {
338
+ insetInlineStart: rect.left,
339
+ insetBlockStart: rect.top,
340
+ inlineSize: rect.width,
341
+ blockSize: rect.height,
342
+ },
343
+ cellElement,
344
+ initialContent: clipboardText,
345
+ });
346
+ cellElement.dispatchEvent(editRequest);
347
+ }
348
+ }
349
+ });
350
+ break;
351
+ }
352
+ }
353
+ }
354
+
282
355
  switch (event.key) {
283
356
  case 'Backspace':
284
357
  case 'Delete': {
285
358
  try {
286
359
  model.setCellData(cell, undefined);
287
360
  event.preventDefault();
361
+ handleSave();
288
362
  } catch {
289
363
  // Delete results in a validation error; don’t prevent default so dx-grid can emit an edit request.
290
364
  }
@@ -292,7 +366,7 @@ const TableMain = forwardRef<TableController, TableMainProps>(
292
366
  }
293
367
  }
294
368
  },
295
- [model],
369
+ [model, dxGrid, handleSave],
296
370
  );
297
371
 
298
372
  const handleAxisResize = useCallback<NonNullable<GridContentProps['onAxisResize']>>(
@@ -321,57 +395,6 @@ const TableMain = forwardRef<TableController, TableMainProps>(
321
395
  }
322
396
  }, [model, dxGrid]);
323
397
 
324
- // TODO(burdon): Factor out?
325
- // TODO(burdon): Generalize to handle other value types (e.g., enums).
326
- const handleQuery = useCallback<NonNullable<TableCellEditorProps['onQuery']>>(
327
- async ({ field, props }, text) => {
328
- if (model && props.referenceSchema && field.referencePath) {
329
- const space = getSpace(model.view);
330
- invariant(space);
331
-
332
- let schema;
333
- if (client) {
334
- schema = client.graph.schemaRegistry.getSchema(props.referenceSchema);
335
- }
336
- if (!schema) {
337
- schema = space.db.schemaRegistry.getSchema(props.referenceSchema);
338
- }
339
-
340
- if (schema) {
341
- const { objects } = await space.db.query(Filter.type(schema)).run();
342
- const options = objects
343
- .map((obj) => {
344
- const value = getValue(obj, field.referencePath!);
345
- if (!value || typeof value !== 'string') {
346
- return undefined;
347
- }
348
-
349
- return {
350
- label: value,
351
- data: obj,
352
- };
353
- })
354
- .filter(isNotFalsy);
355
-
356
- return [
357
- ...options,
358
- {
359
- label: t('create new object label', { text }),
360
- data: createOption(text),
361
- },
362
- ];
363
- }
364
- }
365
-
366
- return [];
367
- },
368
- [model, client, t],
369
- );
370
-
371
- const handleSave = useCallback(() => {
372
- dxGrid?.updateCells(true);
373
- }, [dxGrid]);
374
-
375
398
  if (!model || !modals) {
376
399
  return <span role='none' className='attention-surface' />;
377
400
  }
@@ -382,15 +405,17 @@ const TableMain = forwardRef<TableController, TableMainProps>(
382
405
  model={model}
383
406
  modals={modals}
384
407
  schema={schema}
385
- onEnter={handleEnter}
386
408
  onFocus={handleFocus}
387
- onQuery={handleQuery}
388
409
  onSave={handleSave}
410
+ onCreate={onCreate}
411
+ client={client}
389
412
  />
390
413
  <Grid.Content
391
414
  className={mx('[--dx-grid-base:var(--baseSurface)]', gridSeparatorInlineEnd, gridSeparatorBlockEnd)}
392
415
  frozen={frozen}
393
416
  columns={model.columnMeta.value}
417
+ columnDefault={columnDefault}
418
+ rowDefault={rowDefault}
394
419
  limitRows={model.getRowCount() ?? 0}
395
420
  limitColumns={model.projection.fields.length}
396
421
  overscroll='trap'
@@ -398,12 +423,12 @@ const TableMain = forwardRef<TableController, TableMainProps>(
398
423
  onClick={handleGridClick}
399
424
  onKeyDownCapture={handleKeyDown}
400
425
  onWheelCapture={handleWheel}
426
+ {...(testId && { 'data-testid': testId })}
401
427
  ref={setDxGrid}
402
428
  />
403
429
  <RowActionsMenu model={model} modals={modals} />
404
430
  <ColumnActionsMenu model={model} modals={modals} />
405
431
  <ColumnSettings model={model} modals={modals} onNewColumn={handleNewColumn} />
406
- <CreateRefPanel model={model} modals={modals} />
407
432
  </Grid.Root>
408
433
  );
409
434
  },
@@ -4,7 +4,7 @@
4
4
 
5
5
  import React from 'react';
6
6
 
7
- import { useGridContext, type GridScopedProps } from '@dxos/react-ui-grid';
7
+ import { type GridScopedProps, useGridContext } from '@dxos/react-ui-grid';
8
8
 
9
9
  export type CellValidationMessageProps = {
10
10
  validationError: string | null;
@@ -2,33 +2,124 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { type Schema } from 'effect';
5
+ import * as Schema from 'effect/Schema';
6
6
  import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
7
7
 
8
- import { getSnapshot } from '@dxos/echo-schema';
8
+ import { Filter, Obj } from '@dxos/echo';
9
+ import { Format, Ref, type TypeAnnotation, getValue } from '@dxos/echo/internal';
9
10
  import { invariant } from '@dxos/invariant';
11
+ import { getSnapshot } from '@dxos/live-object';
12
+ import { type Client } from '@dxos/react-client';
13
+ import { getSpace } from '@dxos/react-client/echo';
10
14
  import { Popover } from '@dxos/react-ui';
11
- import { Form } from '@dxos/react-ui-form';
15
+ import { Form, type FormProps } from '@dxos/react-ui-form';
12
16
  import { parseCellIndex, useGridContext } from '@dxos/react-ui-grid';
13
17
  import { type FieldProjection } from '@dxos/schema';
14
- import { getDeep, setDeep } from '@dxos/util';
18
+ import { getDeep, isTruthy, setDeep } from '@dxos/util';
15
19
 
16
- import { type TableModel } from '../../model';
20
+ import { type ModalController, type TableModel } from '../../model';
21
+ import { translationKey } from '../../translations';
17
22
  import { narrowSchema } from '../../util';
18
23
 
19
- type FormCellEditorProps = {
24
+ export type OnCreateHandler = (schema: Schema.Schema.AnyNoContext, values: any) => Parameters<typeof Ref.make>[0];
25
+
26
+ export type FormCellEditorProps = {
20
27
  fieldProjection: FieldProjection;
21
28
  model?: TableModel;
22
29
  schema?: Schema.Schema.AnyNoContext;
23
30
  onSave?: () => void;
31
+ onCreate?: OnCreateHandler;
32
+ client?: Client;
33
+ modals?: ModalController;
24
34
  __gridScope: any;
25
- };
35
+ } & Omit<FormProps<any>, 'values' | 'schema' | 'onCreate'>;
26
36
 
27
- export const FormCellEditor = ({ fieldProjection, model, schema, onSave, __gridScope }: FormCellEditorProps) => {
28
- const { editing: contextEditing, setEditing } = useGridContext('ArrayEditor', __gridScope);
37
+ const createOptionLabel = ['create new object label', { ns: translationKey }] as [string, { ns: string }];
38
+
39
+ export const FormCellEditor = ({
40
+ fieldProjection,
41
+ model,
42
+ schema,
43
+ onSave,
44
+ client,
45
+ modals,
46
+ onCreate,
47
+ __gridScope,
48
+ ...formProps
49
+ }: FormCellEditorProps) => {
50
+ const { editing: contextEditing, setEditing, id: gridId } = useGridContext('ArrayEditor', __gridScope);
29
51
  const [editing, setLocalEditing] = useState(false);
30
52
  const anchorRef = useRef<HTMLButtonElement | null>(null);
31
53
 
54
+ const getSchema = useCallback(
55
+ (typeAnnotation: Pick<TypeAnnotation, 'typename'>) => {
56
+ const space = getSpace(model!.view);
57
+ invariant(space);
58
+
59
+ let schema;
60
+ if (client) {
61
+ schema = client.graph.schemaRegistry.getSchema(typeAnnotation.typename);
62
+ }
63
+ if (!schema) {
64
+ schema = space.db.schemaRegistry.getSchema(typeAnnotation.typename);
65
+ }
66
+ return { space, schema };
67
+ },
68
+ [client, model],
69
+ );
70
+
71
+ const handleQueryRefOptions = useCallback(
72
+ async (typeAnnotation: TypeAnnotation) => {
73
+ const { schema, space } = getSchema(typeAnnotation);
74
+ if (model && schema && space) {
75
+ const { objects } = await space.db.query(Filter.type(schema)).run();
76
+ return objects
77
+ .map((obj) => {
78
+ return {
79
+ dxn: Obj.getDXN(obj),
80
+ label: getValue(obj, fieldProjection.field.referencePath!) || obj.id.toString(),
81
+ };
82
+ })
83
+ .filter(isTruthy);
84
+ }
85
+
86
+ return [];
87
+ },
88
+ [client, model],
89
+ );
90
+
91
+ const originalRow = useMemo(() => {
92
+ if (model && contextEditing) {
93
+ const cell = parseCellIndex(contextEditing.index);
94
+
95
+ // Check if this is a draft cell and get the appropriate row data
96
+ if (model.isDraftCell(cell)) {
97
+ const draftRow = model.draftRows.value[cell.row];
98
+ invariant(draftRow);
99
+ return draftRow.data;
100
+ } else {
101
+ const row = model.getRowAt(cell.row);
102
+ invariant(row);
103
+ return row;
104
+ }
105
+ }
106
+
107
+ return undefined;
108
+ }, [model, contextEditing]);
109
+
110
+ const handleSave = useCallback(
111
+ (values: any) => {
112
+ const path = fieldProjection.field.path;
113
+ const value = getDeep(values, [path]);
114
+ setDeep(originalRow, [path], value);
115
+ contextEditing?.cellElement?.focus();
116
+ setEditing(null);
117
+ setLocalEditing(false);
118
+ onSave?.();
119
+ },
120
+ [fieldProjection.field.path, onSave, contextEditing, originalRow],
121
+ );
122
+
32
123
  useEffect(() => {
33
124
  if (contextEditing && contextEditing.cellElement) {
34
125
  anchorRef.current = (contextEditing.cellElement as HTMLElement).querySelector(
@@ -48,45 +139,49 @@ export const FormCellEditor = ({ fieldProjection, model, schema, onSave, __gridS
48
139
  return narrowSchema(schema, [fieldProjection.field.path]);
49
140
  }, [JSON.stringify(schema), fieldProjection.field.path]); // TODO(burdon): Avoid stringify.
50
141
 
51
- const originalRow = useMemo(() => {
52
- if (model && contextEditing) {
53
- const cell = parseCellIndex(contextEditing.index);
54
- const row = model.getRowAt(cell.row);
55
- invariant(row);
142
+ // NOTE: Important to get a snapshot to eject from the live object.
143
+ const formValues = useMemo(() => (originalRow ? getSnapshot(originalRow) : {}), [originalRow]);
56
144
 
57
- return row;
145
+ const handleOpenChange = useCallback((nextOpen: boolean) => {
146
+ if (nextOpen === false) {
147
+ setEditing(null);
148
+ onSave?.();
58
149
  }
150
+ setLocalEditing(nextOpen);
151
+ }, []);
59
152
 
60
- return undefined;
61
- }, [model, contextEditing]);
62
-
63
- const formValues = useMemo(() => {
64
- if (originalRow) {
65
- // NOTE(ZaymonFC): Important to get a snapshot to eject from the live object.
66
- return getSnapshot(originalRow);
153
+ const refSchema = useMemo(() => {
154
+ if (fieldProjection.props.format === Format.TypeFormat.Ref && fieldProjection.props.referenceSchema) {
155
+ const { schema: refSchema } = getSchema({
156
+ typename: fieldProjection.props.referenceSchema,
157
+ });
158
+ return refSchema;
67
159
  }
68
- }, [originalRow]);
160
+ return null;
161
+ }, [fieldProjection.props.format, fieldProjection.props.referenceSchema, getSchema]);
69
162
 
70
- const handleSave = useCallback(
163
+ const createSchema = useMemo(() => {
164
+ return refSchema ? Schema.omit<any, any, ['id']>('id')(refSchema) : null;
165
+ }, [refSchema]);
166
+
167
+ const handleCreate = useCallback(
71
168
  (values: any) => {
72
- const path = fieldProjection.field.path;
73
- const value = getDeep(values, [path]);
74
- setDeep(originalRow, [path], value);
169
+ if (refSchema && onCreate) {
170
+ const objectWithId = onCreate(refSchema, values);
171
+ if (objectWithId) {
172
+ const ref = Ref.make(objectWithId);
173
+ const path = fieldProjection.field.path;
174
+ setDeep(originalRow, [path], ref);
175
+ }
176
+ }
177
+ contextEditing?.cellElement?.focus();
75
178
  setEditing(null);
76
179
  setLocalEditing(false);
77
180
  onSave?.();
78
181
  },
79
- [fieldProjection.field.path, originalRow],
182
+ [fieldProjection.field.path, onSave, contextEditing, originalRow, refSchema, onCreate],
80
183
  );
81
184
 
82
- const handleOpenChange = useCallback((nextOpen: boolean) => {
83
- if (nextOpen === false) {
84
- setEditing(null);
85
- onSave?.();
86
- }
87
- setLocalEditing(nextOpen);
88
- }, []);
89
-
90
185
  if (!editing) {
91
186
  return null;
92
187
  }
@@ -94,12 +189,28 @@ export const FormCellEditor = ({ fieldProjection, model, schema, onSave, __gridS
94
189
  return (
95
190
  <Popover.Root open={editing} onOpenChange={handleOpenChange}>
96
191
  <Popover.VirtualTrigger virtualRef={anchorRef} />
97
- <Popover.Content tabIndex={-1} classNames='popover-card-width density-fine'>
98
- <Popover.Arrow />
99
- <Popover.Viewport>
100
- <Form values={formValues} schema={narrowedSchema as any} onSave={handleSave} />
101
- </Popover.Viewport>
102
- </Popover.Content>
192
+ <Popover.Portal>
193
+ <Popover.Content tabIndex={-1} classNames='popover-card-width density-fine'>
194
+ <Popover.Arrow />
195
+ <Popover.Viewport>
196
+ <Form
197
+ autoFocus
198
+ values={formValues}
199
+ schema={narrowedSchema as any}
200
+ onSave={handleSave}
201
+ {...formProps}
202
+ onQueryRefOptions={handleQueryRefOptions}
203
+ {...(createSchema && {
204
+ onCreate: handleCreate,
205
+ createSchema,
206
+ createInitialValuePath: fieldProjection.field.referencePath,
207
+ createOptionIcon: 'ph--plus--regular',
208
+ createOptionLabel,
209
+ })}
210
+ />
211
+ </Popover.Viewport>
212
+ </Popover.Content>
213
+ </Popover.Portal>
103
214
  </Popover.Root>
104
215
  );
105
216
  };