@dxos/react-ui-table 0.8.4-main.bc674ce → 0.8.4-main.bcb3aa67d6

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 (108) hide show
  1. package/dist/lib/browser/{chunk-OZBLY7XM.mjs → chunk-BF54SSNL.mjs} +12 -18
  2. package/dist/lib/browser/chunk-BF54SSNL.mjs.map +7 -0
  3. package/dist/lib/browser/{chunk-I5IFW7UF.mjs → chunk-EK6D2CVM.mjs} +37 -32
  4. package/dist/lib/browser/chunk-EK6D2CVM.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +200 -173
  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 +4 -4
  9. package/dist/lib/browser/testing/index.mjs.map +2 -2
  10. package/dist/lib/browser/types/index.mjs +1 -1
  11. package/dist/lib/node-esm/{chunk-DDJKYJFY.mjs → chunk-LYYDF6JP.mjs} +37 -32
  12. package/dist/lib/node-esm/chunk-LYYDF6JP.mjs.map +7 -0
  13. package/dist/lib/node-esm/{chunk-WNZSKIRF.mjs → chunk-SIRDIPHZ.mjs} +12 -18
  14. package/dist/lib/node-esm/chunk-SIRDIPHZ.mjs.map +7 -0
  15. package/dist/lib/node-esm/index.mjs +200 -173
  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 +4 -4
  19. package/dist/lib/node-esm/testing/index.mjs.map +2 -2
  20. package/dist/lib/node-esm/types/index.mjs +1 -1
  21. package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
  22. package/dist/types/src/components/Table/DynamicTable.d.ts +5 -4
  23. package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
  24. package/dist/types/src/components/Table/DynamicTable.stories.d.ts +20 -22
  25. package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
  26. package/dist/types/src/components/Table/Relations.stories.d.ts +80 -82
  27. package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -1
  28. package/dist/types/src/components/Table/Table.d.ts +28 -21
  29. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  30. package/dist/types/src/components/Table/Table.stories.d.ts +82 -84
  31. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  32. package/dist/types/src/components/Table/TableContent.d.ts +21 -0
  33. package/dist/types/src/components/Table/TableContent.d.ts.map +1 -0
  34. package/dist/types/src/components/Table/TableToolbar.d.ts +25 -0
  35. package/dist/types/src/components/Table/TableToolbar.d.ts.map +1 -0
  36. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +3 -3
  37. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
  38. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +2 -2
  39. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  40. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +22 -24
  41. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  42. package/dist/types/src/components/index.d.ts +0 -1
  43. package/dist/types/src/components/index.d.ts.map +1 -1
  44. package/dist/types/src/hooks/useAddRow.d.ts +1 -1
  45. package/dist/types/src/hooks/useAddRow.d.ts.map +1 -1
  46. package/dist/types/src/hooks/useProjectionModel.d.ts +1 -1
  47. package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -1
  48. package/dist/types/src/hooks/useTableModel.d.ts +1 -1
  49. package/dist/types/src/model/table-model.d.ts +5 -4
  50. package/dist/types/src/model/table-model.d.ts.map +1 -1
  51. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  52. package/dist/types/src/playwright/TableManager.d.ts +2 -1
  53. package/dist/types/src/playwright/TableManager.d.ts.map +1 -1
  54. package/dist/types/src/testing/useTestTableModel.d.ts +2 -2
  55. package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -1
  56. package/dist/types/src/translations.d.ts +19 -21
  57. package/dist/types/src/translations.d.ts.map +1 -1
  58. package/dist/types/src/types/Table.d.ts +1 -6
  59. package/dist/types/src/types/Table.d.ts.map +1 -1
  60. package/dist/types/src/util/dynamic-table.d.ts +6 -5
  61. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  62. package/dist/types/src/util/dynamic-table.test.d.ts +2 -0
  63. package/dist/types/src/util/dynamic-table.test.d.ts.map +1 -0
  64. package/dist/types/src/util/query-ast.d.ts.map +1 -1
  65. package/dist/types/tsconfig.tsbuildinfo +1 -1
  66. package/package.json +43 -43
  67. package/src/components/Table/ColumnActionsMenu.tsx +5 -5
  68. package/src/components/Table/ColumnSettings.tsx +3 -4
  69. package/src/components/Table/DynamicTable.stories.tsx +6 -5
  70. package/src/components/Table/DynamicTable.tsx +13 -19
  71. package/src/components/Table/Relations.stories.tsx +35 -25
  72. package/src/components/Table/Table.stories.tsx +61 -40
  73. package/src/components/Table/Table.tsx +41 -434
  74. package/src/components/Table/TableContent.tsx +422 -0
  75. package/src/components/{TableToolbar → Table}/TableToolbar.tsx +35 -42
  76. package/src/components/TableCellEditor/CellValidationMessage.tsx +4 -4
  77. package/src/components/TableCellEditor/FormCellEditor.tsx +12 -6
  78. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +6 -5
  79. package/src/components/TableCellEditor/TableCellEditor.tsx +10 -5
  80. package/src/components/index.ts +0 -1
  81. package/src/components/testing.ts +1 -1
  82. package/src/hooks/useAddRow.ts +1 -1
  83. package/src/hooks/useProjectionModel.ts +3 -3
  84. package/src/hooks/useTableModel.ts +1 -1
  85. package/src/model/table-model.test.ts +5 -5
  86. package/src/model/table-model.ts +13 -12
  87. package/src/model/table-presentation.test.ts +5 -5
  88. package/src/model/table-presentation.ts +12 -11
  89. package/src/playwright/TableManager.ts +10 -3
  90. package/src/playwright/playwright.config.ts +1 -1
  91. package/src/playwright/smoke.spec.ts +45 -2
  92. package/src/testing/useTestTableModel.ts +4 -4
  93. package/src/translations.ts +19 -21
  94. package/src/types/Table.ts +11 -22
  95. package/src/util/dynamic-table.test.ts +70 -0
  96. package/src/util/dynamic-table.ts +17 -16
  97. package/src/util/query-ast.ts +3 -0
  98. package/src/util/table-buttons.ts +1 -1
  99. package/src/util/table-controls.ts +2 -2
  100. package/dist/lib/browser/chunk-I5IFW7UF.mjs.map +0 -7
  101. package/dist/lib/browser/chunk-OZBLY7XM.mjs.map +0 -7
  102. package/dist/lib/node-esm/chunk-DDJKYJFY.mjs.map +0 -7
  103. package/dist/lib/node-esm/chunk-WNZSKIRF.mjs.map +0 -7
  104. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +0 -21
  105. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +0 -1
  106. package/dist/types/src/components/TableToolbar/index.d.ts +0 -2
  107. package/dist/types/src/components/TableToolbar/index.d.ts.map +0 -1
  108. package/src/components/TableToolbar/index.ts +0 -5
@@ -0,0 +1,422 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { Atom, RegistryContext, useAtomValue } from '@effect-atom/atom-react';
6
+ import * as String from 'effect/String';
7
+ import React, { type MouseEvent, type WheelEvent, useCallback, useContext, useEffect, useMemo, useState } from 'react';
8
+
9
+ import { type Type } from '@dxos/echo';
10
+ import { log } from '@dxos/log';
11
+ import { useAttention } from '@dxos/react-ui-attention';
12
+ import {
13
+ type DxGridAxisMeta,
14
+ type DxGridElement,
15
+ type DxGridPlane,
16
+ type DxGridPlaneRange,
17
+ Grid,
18
+ type GridContentProps,
19
+ closestCell,
20
+ gridSeparatorBlockEnd,
21
+ gridSeparatorInlineEnd,
22
+ } from '@dxos/react-ui-grid';
23
+ import { DxEditRequest } from '@dxos/react-ui-grid';
24
+ import { composable, composableProps, mx } from '@dxos/ui-theme';
25
+
26
+ import { type InsertRowResult, ModalController, type TableModel, type TablePresentation } from '../../model';
27
+ import { tableButtons, tableControls } from '../../util';
28
+ import { type OnCreateHandler, type TableCellEditorProps, TableValueEditor } from '../TableCellEditor';
29
+
30
+ import { ColumnActionsMenu } from './ColumnActionsMenu';
31
+ import { ColumnSettings } from './ColumnSettings';
32
+ import { RowActionsMenu } from './RowActionsMenu';
33
+ import { TableController, useTableContext } from './Table';
34
+
35
+ const columnDefault = { grid: { minSize: 80, maxSize: 640 } };
36
+ const rowDefault = { frozenRowsStart: { readonly: true, focusUnfurl: false } };
37
+ const emptyColumnMeta = Atom.make<DxGridAxisMeta>({ grid: {} });
38
+ const emptyRowsAtom = Atom.make<unknown[]>([]);
39
+ const emptyCellUpdateAtom = Atom.make<number>(0);
40
+
41
+ //
42
+ // Content
43
+ //
44
+
45
+ const TABLE_CONTENT_NAME = 'Table.Content';
46
+
47
+ export type TableContentProps = {
48
+ schema?: Type.AnyEntity;
49
+ model?: TableModel;
50
+ presentation?: TablePresentation;
51
+ // TODO(burdon): Factor out attention.
52
+ ignoreAttention?: boolean;
53
+ onCreate?: OnCreateHandler;
54
+ onRowClick?: (row: any) => void;
55
+ testId?: string;
56
+ };
57
+
58
+ export const TableContent = composable<HTMLDivElement, TableContentProps>(
59
+ ({ schema, model, presentation, ignoreAttention, onCreate, onRowClick, testId, ...props }, forwardedRef) => {
60
+ const registry = useContext(RegistryContext);
61
+ const [dxGrid, setDxGrid] = useState<DxGridElement | null>(null);
62
+ const { hasAttention } = useAttention(model?.id ?? 'table');
63
+ const modals = useMemo(() => new ModalController(registry), [registry]);
64
+ const columnMeta = useAtomValue(model?.columnMeta ?? emptyColumnMeta);
65
+ // Subscribe to rows atom to trigger re-render when rows change.
66
+ const rows = useAtomValue(model?.rows ?? emptyRowsAtom);
67
+ // Subscribe to cell update counter to trigger re-render when individual cells change.
68
+ const cellUpdateCounter = useAtomValue(model?.cellUpdate ?? emptyCellUpdateAtom);
69
+ // Derive column count from columnMeta (reactive) with fallback.
70
+ const columnCount = Object.keys(columnMeta.grid).length || model?.projection.getFields().length || 0;
71
+
72
+ const draftRowCount = model?.getDraftRowCount() ?? 0;
73
+
74
+ const frozen = useMemo(() => {
75
+ const noActionColumn =
76
+ model?.features.dataEditable === false &&
77
+ model?.features.schemaEditable === false &&
78
+ model.rowActions.length === 0;
79
+
80
+ return {
81
+ frozenRowsStart: 1,
82
+ frozenRowsEnd: Math.max(1, draftRowCount),
83
+ frozenColsStart: model?.features.selection.enabled ? 1 : 0,
84
+ frozenColsEnd: noActionColumn ? 0 : 1,
85
+ };
86
+ }, [model, draftRowCount]);
87
+
88
+ const getCells = useCallback<NonNullable<GridContentProps['getCells']>>(
89
+ (range: DxGridPlaneRange, plane: DxGridPlane) => presentation?.getCells(range, plane) ?? {},
90
+ [presentation],
91
+ );
92
+
93
+ useEffect(() => {
94
+ if (!dxGrid || !presentation) {
95
+ return;
96
+ }
97
+
98
+ dxGrid.getCells = getCells;
99
+ }, [dxGrid, presentation, getCells]);
100
+
101
+ // Trigger grid update when rows or cell values change.
102
+ useEffect(() => {
103
+ if (!dxGrid) {
104
+ return;
105
+ }
106
+
107
+ dxGrid.updateCells(true);
108
+ dxGrid.requestUpdate();
109
+ }, [dxGrid, rows, cellUpdateCounter]);
110
+
111
+ const handleInsertRowResult = useCallback(
112
+ (insertResult?: InsertRowResult) => {
113
+ if (insertResult === 'draft') {
114
+ requestAnimationFrame(() => {
115
+ dxGrid?.setFocus({ plane: 'frozenRowsEnd', col: 0, row: 0 });
116
+ dxGrid?.refocus();
117
+ });
118
+ } else {
119
+ requestAnimationFrame(() => {
120
+ dxGrid?.setFocus({ plane: 'grid', col: 0, row: model ? model.getRowCount() - 1 : 0 });
121
+ dxGrid?.refocus();
122
+ });
123
+ }
124
+ },
125
+ [model, dxGrid],
126
+ );
127
+
128
+ const handleSave = useCallback(() => {
129
+ dxGrid?.updateCells(true);
130
+ dxGrid?.requestUpdate();
131
+ }, [dxGrid]);
132
+
133
+ const handleSaveDraftRow = useCallback(
134
+ (rowIndex = 0) => {
135
+ if (model && dxGrid) {
136
+ const didCommitSuccessfully = model.commitDraftRow(rowIndex);
137
+ if (didCommitSuccessfully) {
138
+ requestAnimationFrame(() => {
139
+ dxGrid.scrollToEndRow();
140
+ });
141
+ }
142
+ }
143
+ },
144
+ [model, dxGrid],
145
+ );
146
+
147
+ const handleGridClick = useCallback(
148
+ (event: MouseEvent) => {
149
+ const cell = closestCell(event.target as HTMLElement);
150
+ if (cell) {
151
+ const { row: rowIndex, plane } = cell;
152
+ if (onRowClick) {
153
+ if (plane === 'grid') {
154
+ const row = model?.getRowAt(rowIndex);
155
+ row && onRowClick(row);
156
+ } else {
157
+ onRowClick(cell);
158
+ }
159
+ }
160
+
161
+ if (model?.features.selection.enabled && model?.selection.selectionMode === 'single') {
162
+ model.selection.toggleSelectionForRowIndex(rowIndex);
163
+ }
164
+ }
165
+
166
+ if (!modals) {
167
+ return;
168
+ }
169
+
170
+ const buttonSelector = Object.values(tableButtons)
171
+ .map((button) => `button[${button.attr}]`)
172
+ .join(',');
173
+
174
+ const tableButtonElement = (event.target as HTMLElement).closest(buttonSelector) as HTMLElement;
175
+ if (tableButtonElement) {
176
+ const button = Object.values(tableButtons).find((btn) => tableButtonElement.hasAttribute(btn.attr));
177
+ if (!button) {
178
+ return;
179
+ }
180
+
181
+ modals.setTrigger(tableButtonElement);
182
+ const data = button.getData(tableButtonElement);
183
+ switch (data.type) {
184
+ case 'rowMenu': {
185
+ modals.showRowMenu(data.rowIndex);
186
+ break;
187
+ }
188
+ case 'columnSettings': {
189
+ modals.showColumnMenu(data.fieldId);
190
+ break;
191
+ }
192
+ case 'newColumn': {
193
+ modals.showColumnCreator();
194
+ break;
195
+ }
196
+ case 'sort': {
197
+ model?.toggleSort(data.fieldId);
198
+ break;
199
+ }
200
+ case 'saveDraftRow': {
201
+ handleSaveDraftRow();
202
+ break;
203
+ }
204
+ }
205
+ return;
206
+ }
207
+
208
+ const controlSelector = Object.values(tableControls)
209
+ .map((control) => `input[${control.attr}]`)
210
+ .join(',');
211
+
212
+ const controlElement = (event.target as HTMLElement).closest(controlSelector) as HTMLElement;
213
+ if (controlElement) {
214
+ const control = Object.values(tableControls).find((ctrl) => controlElement.hasAttribute(ctrl.attr));
215
+ if (!control) {
216
+ return;
217
+ }
218
+
219
+ const data = control.getData(controlElement);
220
+ switch (data.type) {
221
+ case 'checkbox': {
222
+ if (data.header) {
223
+ model?.selection.setSelection(model.selection.allRowsSelected ? 'none' : 'all');
224
+ } else {
225
+ model?.selection.toggleSelectionForRowIndex(data.rowIndex);
226
+ }
227
+ break;
228
+ }
229
+ case 'switch': {
230
+ if (model) {
231
+ model.updateCellData({ row: data.rowIndex, col: data.colIndex }, (value) => !value);
232
+ }
233
+ break;
234
+ }
235
+ }
236
+ }
237
+ },
238
+ [model, modals, dxGrid],
239
+ );
240
+
241
+ const handleFocus = useCallback<NonNullable<TableCellEditorProps['onFocus']>>(
242
+ (increment, delta, cell) => {
243
+ if (dxGrid && model) {
244
+ if (cell?.plane === 'grid' && cell?.row >= model.getRowCount() - 1 && increment !== 'col') {
245
+ handleInsertRowResult(draftRowCount < 1 ? model.insertRow() : 'final');
246
+ } else if (cell?.plane === 'frozenRowsEnd' && increment === 'row') {
247
+ handleSaveDraftRow(cell.row);
248
+ handleInsertRowResult(model.insertRow());
249
+ } else {
250
+ dxGrid.refocus(increment, delta);
251
+ }
252
+ }
253
+ },
254
+ [dxGrid, model],
255
+ );
256
+
257
+ const handleKeyDown = useCallback<NonNullable<GridContentProps['onKeyDown']>>(
258
+ (event) => {
259
+ if (!model?.features.dataEditable) {
260
+ return;
261
+ }
262
+
263
+ const cell = closestCell(event.target);
264
+ if (!model || !cell) {
265
+ return;
266
+ }
267
+
268
+ // Handle Meta+C (Copy) and Meta+V (Paste) commands
269
+ if (event.metaKey || event.ctrlKey) {
270
+ switch (event.key) {
271
+ case 'c': {
272
+ // Copy focused cell's text content to clipboard
273
+ try {
274
+ const cellData = model.getCellData(cell);
275
+ const textContent = cellData?.toString() ?? '';
276
+ void navigator.clipboard.writeText(textContent);
277
+ event.preventDefault();
278
+ } catch (error) {
279
+ log.catch(error);
280
+ }
281
+ break;
282
+ }
283
+ case 'v': {
284
+ // Paste clipboard content to focused cell
285
+ event.preventDefault();
286
+ void navigator.clipboard.readText().then((clipboardText) => {
287
+ try {
288
+ // Attempt to set the cell's content to clipboard content
289
+ model.setCellData(cell, String.trim(clipboardText).replace(/[\n\r]+/, ' '));
290
+ handleSave();
291
+ } catch {
292
+ // If validation fails, emit a DxEditRequest event with initialContent from clipboard
293
+ // TODO(thure): Should `dx-grid` expose a method like this?
294
+ const cellElement = (event.target as HTMLElement).closest(
295
+ '[data-dx-grid-action="cell"]',
296
+ ) as HTMLElement;
297
+ if (cellElement) {
298
+ const rect = cellElement.getBoundingClientRect();
299
+ const editRequest = new DxEditRequest({
300
+ cellIndex: `${cell.plane},${cell.col},${cell.row}`,
301
+ cellBox: {
302
+ insetInlineStart: rect.left,
303
+ insetBlockStart: rect.top,
304
+ inlineSize: rect.width,
305
+ blockSize: rect.height,
306
+ },
307
+ cellElement,
308
+ initialContent: clipboardText,
309
+ });
310
+ cellElement.dispatchEvent(editRequest);
311
+ }
312
+ }
313
+ });
314
+ break;
315
+ }
316
+ }
317
+ }
318
+
319
+ switch (event.key) {
320
+ case 'Backspace':
321
+ case 'Delete': {
322
+ try {
323
+ model.setCellData(cell, undefined);
324
+ event.preventDefault();
325
+ handleSave();
326
+ } catch {
327
+ // Delete results in a validation error; don’t prevent default so dx-grid can emit an edit request.
328
+ }
329
+ break;
330
+ }
331
+ }
332
+ },
333
+ [model, dxGrid, handleSave],
334
+ );
335
+
336
+ const handleAxisResize = useCallback<NonNullable<GridContentProps['onAxisResize']>>(
337
+ (event) => {
338
+ if (event.axis === 'col') {
339
+ const columnIndex = parseInt(event.index, 10);
340
+ model?.setColumnWidth(columnIndex, event.size);
341
+ }
342
+ },
343
+ [model],
344
+ );
345
+
346
+ const handleWheel = useCallback(
347
+ (event: WheelEvent) => {
348
+ if (!ignoreAttention && !hasAttention) {
349
+ event.stopPropagation();
350
+ }
351
+ },
352
+ [hasAttention, ignoreAttention],
353
+ );
354
+
355
+ const handleNewColumn = useCallback(() => {
356
+ const columns = model?.getColumnCount();
357
+ if (dxGrid && columns) {
358
+ dxGrid.scrollToColumn(columns - 1);
359
+ }
360
+ }, [model, dxGrid]);
361
+
362
+ // Publish controller to Root.
363
+ const { controllerRef } = useTableContext(TABLE_CONTENT_NAME);
364
+ controllerRef.current = useMemo<TableController>(() => {
365
+ if (!dxGrid || !presentation) {
366
+ return {};
367
+ }
368
+
369
+ return {
370
+ update: (cell) => {
371
+ if (cell) {
372
+ dxGrid.updateIfWithinBounds(cell, true);
373
+ } else {
374
+ dxGrid.updateCells(true);
375
+ dxGrid.requestUpdate();
376
+ }
377
+ },
378
+ handleInsertRowResult,
379
+ };
380
+ }, [dxGrid, model, presentation]);
381
+
382
+ if (!model || !modals) {
383
+ return <span role='none' className='dx-attention-surface' />;
384
+ }
385
+
386
+ return (
387
+ <div {...composableProps(props, { classNames: 'dx-container relative' })} ref={forwardedRef}>
388
+ <Grid.Root id={model.id ?? 'table-grid'}>
389
+ <TableValueEditor
390
+ model={model}
391
+ modals={modals}
392
+ schema={schema}
393
+ onFocus={handleFocus}
394
+ onSave={handleSave}
395
+ onCreate={onCreate}
396
+ />
397
+ <Grid.Content
398
+ className={mx('[--dx-grid-base:var(--base-surface)]', gridSeparatorInlineEnd, gridSeparatorBlockEnd)}
399
+ frozen={frozen}
400
+ columns={columnMeta}
401
+ columnDefault={columnDefault}
402
+ rowDefault={rowDefault}
403
+ limitRows={rows.length}
404
+ limitColumns={columnCount}
405
+ overscroll='trap'
406
+ onAxisResize={handleAxisResize}
407
+ onClick={handleGridClick}
408
+ onKeyDownCapture={handleKeyDown}
409
+ onWheelCapture={handleWheel}
410
+ {...(testId && { 'data-testid': testId })}
411
+ ref={setDxGrid}
412
+ />
413
+ <RowActionsMenu model={model} modals={modals} />
414
+ <ColumnActionsMenu model={model} modals={modals} />
415
+ <ColumnSettings model={model} modals={modals} onNewColumn={handleNewColumn} />
416
+ </Grid.Root>
417
+ </div>
418
+ );
419
+ },
420
+ );
421
+
422
+ TableContent.displayName = TABLE_CONTENT_NAME;
@@ -5,18 +5,18 @@
5
5
  import { Atom, RegistryContext } from '@effect-atom/atom-react';
6
6
  import React, { useContext, useEffect, useMemo } from 'react';
7
7
 
8
- import { type ThemedClassName } from '@dxos/react-ui';
9
8
  import {
10
9
  type ActionGraphEdges,
11
10
  type ActionGraphNodes,
12
11
  type ActionGraphProps,
12
+ Menu,
13
13
  type MenuAction,
14
- MenuProvider,
15
- ToolbarMenu,
14
+ MenuRootProps,
16
15
  createGapSeparator,
17
16
  createMenuAction,
18
17
  useMenuActions,
19
18
  } from '@dxos/react-ui-menu';
19
+ import { composable, composableProps } from '@dxos/ui-theme';
20
20
 
21
21
  import { translationKey } from '../../translations';
22
22
 
@@ -28,17 +28,6 @@ export type TableToolbarActionType = 'add-row' | 'comment' | 'save-view';
28
28
 
29
29
  type TableToolbarState = Partial<{ viewDirty: boolean }>;
30
30
 
31
- // TODO(burdon): Radix style layout.
32
-
33
- export type TableToolbarProps = ThemedClassName<
34
- TableToolbarState & {
35
- onAdd?: () => void;
36
- onSave?: () => void;
37
- attendableId?: string;
38
- customActions?: Atom.Atom<ActionGraphProps>;
39
- }
40
- >;
41
-
42
31
  const createTableToolbarActions = ({
43
32
  stateAtom,
44
33
  onAdd,
@@ -56,7 +45,7 @@ const createTableToolbarActions = ({
56
45
  const add = createMenuAction<TableToolbarActionProperties>('add-row', onAdd, {
57
46
  type: 'add-row' as const,
58
47
  icon: 'ph--plus--regular',
59
- label: ['add row label', { ns: translationKey }],
48
+ label: ['add-row.label', { ns: translationKey }],
60
49
  testId: 'table.toolbar.add-row',
61
50
  });
62
51
  nodes.push(add);
@@ -66,7 +55,7 @@ const createTableToolbarActions = ({
66
55
  const save = createMenuAction<TableToolbarActionProperties>('save-view', onSave, {
67
56
  type: 'save-view' as const,
68
57
  icon: 'ph--floppy-disk--regular',
69
- label: ['save view label', { ns: translationKey }],
58
+ label: ['save-view.label', { ns: translationKey }],
70
59
  testId: 'table.toolbar.save-view',
71
60
  iconOnly: false,
72
61
  hidden: !state.viewDirty,
@@ -75,7 +64,7 @@ const createTableToolbarActions = ({
75
64
  }
76
65
  const gap = createGapSeparator();
77
66
  nodes.push(...gap.nodes);
78
- const edges: ActionGraphEdges = nodes.map(({ id: target }) => ({ source: 'root', target }));
67
+ const edges: ActionGraphEdges = nodes.map(({ id: target }) => ({ source: 'root', target, relation: 'child' }));
79
68
  if (customActions) {
80
69
  const custom = get(customActions);
81
70
  nodes.push(...custom.nodes);
@@ -88,31 +77,35 @@ const createTableToolbarActions = ({
88
77
  };
89
78
  });
90
79
 
91
- export const TableToolbar = ({
92
- classNames,
93
- viewDirty,
94
- attendableId,
95
- onAdd,
96
- onSave,
97
- customActions,
98
- }: TableToolbarProps) => {
99
- const registry = useContext(RegistryContext);
100
- const stateAtom = useMemo(() => Atom.make<TableToolbarState>({ viewDirty }), []);
80
+ export type TableToolbarProps = Pick<MenuRootProps, 'attendableId'> &
81
+ TableToolbarState & {
82
+ onAdd?: () => void;
83
+ onSave?: () => void;
84
+ customActions?: Atom.Atom<ActionGraphProps>;
85
+ };
86
+
87
+ export const TableToolbar = composable<HTMLDivElement, TableToolbarProps>(
88
+ ({ attendableId, viewDirty, onAdd, onSave, customActions, ...props }, forwardedRef) => {
89
+ const registry = useContext(RegistryContext);
90
+ const stateAtom = useMemo(() => Atom.make<TableToolbarState>({ viewDirty }), []);
91
+
92
+ // Update state.viewDirty when the prop changes.
93
+ useEffect(() => {
94
+ registry.set(stateAtom, { viewDirty });
95
+ }, [registry, stateAtom, viewDirty]);
101
96
 
102
- // Update state.viewDirty when the prop changes.
103
- useEffect(() => {
104
- registry.set(stateAtom, { viewDirty });
105
- }, [registry, stateAtom, viewDirty]);
97
+ const actionsCreator = useMemo(
98
+ () => createTableToolbarActions({ stateAtom, onAdd, onSave, customActions }),
99
+ [stateAtom, onAdd, onSave, customActions],
100
+ );
101
+ const menuActions = useMenuActions(actionsCreator);
106
102
 
107
- const actionsCreator = useMemo(
108
- () => createTableToolbarActions({ stateAtom, onAdd, onSave, customActions }),
109
- [stateAtom, onAdd, onSave, customActions],
110
- );
111
- const menu = useMenuActions(actionsCreator);
103
+ return (
104
+ <Menu.Root {...menuActions} attendableId={attendableId}>
105
+ <Menu.Toolbar {...composableProps(props)} ref={forwardedRef} />
106
+ </Menu.Root>
107
+ );
108
+ },
109
+ );
112
110
 
113
- return (
114
- <MenuProvider {...menu} attendableId={attendableId}>
115
- <ToolbarMenu classNames={classNames} />
116
- </MenuProvider>
117
- );
118
- };
111
+ TableToolbar.displayName = 'TableToolbar';
@@ -22,15 +22,15 @@ export const CellValidationMessage = ({
22
22
  return null;
23
23
  }
24
24
 
25
- const bgClass = variant === 'error' ? 'bg-errorSurface' : 'bg-warningSurface';
26
- const textClass = variant === 'error' ? 'text-errorSurfaceText' : 'text-warningSurfaceText';
25
+ const bgClass = variant === 'error' ? 'bg-error-surface' : 'bg-warning-surface';
26
+ const textClass = variant === 'error' ? 'text-error-surface-text' : 'text-warning-surface-text';
27
27
 
28
28
  return (
29
29
  <div
30
30
  role='none'
31
- className={`absolute ${bgClass} ${textClass} rounded-bs-sm text-xs p-1`}
31
+ className={`absolute ${bgClass} ${textClass} rounded-h-sm text-xs p-1`}
32
32
  style={{
33
- ...{ '--dx-gridCellWidth': `${box?.inlineSize ?? 200}px` },
33
+ ...{ '--dx-grid-cell-width': `${box?.inlineSize ?? 200}px` },
34
34
  zIndex: 10,
35
35
  insetBlockEnd: `calc(100% - ${box.insetBlockStart}px)`,
36
36
  insetInlineStart: box.insetInlineStart,
@@ -18,11 +18,11 @@ import { type ModalController, type TableModel, type TableRow } from '../../mode
18
18
  import { translationKey } from '../../translations';
19
19
  import { narrowSchema } from '../../util';
20
20
 
21
- const createOptionLabel: Label = ['create new object label', { ns: translationKey }];
21
+ const createOptionLabel: Label = ['create-new-object.label', { ns: translationKey }];
22
22
 
23
- export type OnCreateHandler = (schema: Type.Entity.Any, values: any) => Parameters<typeof Ref.make>[0];
23
+ export type OnCreateHandler = (schema: Type.AnyEntity, values: any) => Parameters<typeof Ref.make>[0];
24
24
 
25
- export type FormCellEditorProps<T extends Type.Entity.Any = Type.Entity.Any> = {
25
+ export type FormCellEditorProps<T extends Type.AnyEntity = Type.AnyEntity> = {
26
26
  __gridScope: any;
27
27
  schema?: T;
28
28
  model?: TableModel;
@@ -32,7 +32,7 @@ export type FormCellEditorProps<T extends Type.Entity.Any = Type.Entity.Any> = {
32
32
  onCreate?: OnCreateHandler;
33
33
  } & Omit<FormRootProps<any>, 'values' | 'schema' | 'onCreate'>;
34
34
 
35
- export const FormCellEditor = <T extends Type.Entity.Any = Type.Entity.Any>({
35
+ export const FormCellEditor = <T extends Type.AnyEntity = Type.AnyEntity>({
36
36
  __gridScope,
37
37
  schema,
38
38
  model,
@@ -85,8 +85,12 @@ export const FormCellEditor = <T extends Type.Entity.Any = Type.Entity.Any>({
85
85
 
86
86
  // NOTE: Important to get a mutable deep clone to eject from the echo object.
87
87
  // TODO(wittjosiah): Consider using something like Obj.clone for this use case.
88
- const formValues = useMemo(() => (originalRow ? JSON.parse(JSON.stringify(originalRow)) : {}), [originalRow]);
88
+ const initialFormValues = useMemo(() => (originalRow ? JSON.parse(JSON.stringify(originalRow)) : {}), [originalRow]);
89
+ const [formValues, setFormValues] = useState<any>(initialFormValues);
89
90
 
91
+ const handleValuesChanged = useCallback<NonNullable<FormRootProps<any>['onValuesChanged']>>((values) => {
92
+ setFormValues(values);
93
+ }, []);
90
94
  const handleOpenChange = useCallback((nextOpen: boolean) => {
91
95
  if (nextOpen === false) {
92
96
  setEditing(null);
@@ -162,7 +166,7 @@ export const FormCellEditor = <T extends Type.Entity.Any = Type.Entity.Any>({
162
166
  <Popover.Root open={editing} onOpenChange={handleOpenChange}>
163
167
  <Popover.VirtualTrigger virtualRef={anchorRef} />
164
168
  <Popover.Portal>
165
- <Popover.Content tabIndex={-1} classNames='popover-card-width density-fine'>
169
+ <Popover.Content tabIndex={-1} classNames='dx-card-popover-width dx-density-fine'>
166
170
  <Popover.Arrow />
167
171
  <Popover.Viewport>
168
172
  <Form.Root
@@ -170,6 +174,8 @@ export const FormCellEditor = <T extends Type.Entity.Any = Type.Entity.Any>({
170
174
  autoFocus
171
175
  schema={narrowedSchema}
172
176
  values={formValues}
177
+ onValuesChanged={handleValuesChanged}
178
+ projection={model?.projection}
173
179
  createInitialValuePath={fieldProjection.field.referencePath}
174
180
  createOptionIcon='ph--plus--regular'
175
181
  createOptionLabel={createOptionLabel}
@@ -6,11 +6,12 @@ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
6
  import React from 'react';
7
7
 
8
8
  import { Obj } from '@dxos/echo';
9
+ import { View } from '@dxos/echo';
9
10
  import { faker } from '@dxos/random';
10
11
  import { withClientProvider } from '@dxos/react-client/testing';
11
12
  import { withLayout, withTheme } from '@dxos/react-ui/testing';
12
13
  import { Grid, type GridEditing, defaultRowSize } from '@dxos/react-ui-grid';
13
- import { View } from '@dxos/schema';
14
+ import { ViewModel } from '@dxos/schema';
14
15
  import { Task } from '@dxos/types';
15
16
 
16
17
  import { useTestTableModel } from '../../testing';
@@ -19,12 +20,12 @@ import { Table } from '../../types';
19
20
 
20
21
  import { TableCellEditor } from './TableCellEditor';
21
22
 
22
- type StoryProps = {
23
+ type DefaultStoryProps = {
23
24
  editing: GridEditing;
24
25
  };
25
26
 
26
27
  // TODO(burdon): Broken layout.
27
- const DefaultStory = ({ editing }: StoryProps) => {
28
+ const DefaultStory = ({ editing }: DefaultStoryProps) => {
28
29
  const { model, table } = useTestTableModel();
29
30
 
30
31
  if (!model || !table) {
@@ -45,14 +46,14 @@ const meta = {
45
46
  component: DefaultStory,
46
47
  render: DefaultStory,
47
48
  decorators: [
48
- withTheme,
49
+ withTheme(),
49
50
  withLayout({ layout: 'column' }),
50
51
  withClientProvider({
51
52
  types: [View.View, Task.Task, Table.Table],
52
53
  createIdentity: true,
53
54
  createSpace: true,
54
55
  onCreateSpace: async ({ space }) => {
55
- const { view, jsonSchema } = await View.makeFromDatabase({ db: space.db, typename: Task.Task.typename });
56
+ const { view, jsonSchema } = await ViewModel.makeFromDatabase({ db: space.db, typename: Task.Task.typename });
56
57
  const table = Table.make({ view, jsonSchema });
57
58
  space.db.add(table);
58
59
  Array.from({ length: 10 }).forEach(() => {