@dxos/react-ui-table 0.8.4-main.c1de068 → 0.8.4-main.e098934

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