@dxos/react-ui-table 0.8.3-staging.0fa589b → 0.8.4-main.28f8d3d

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-L2LSB6KV.mjs +18 -0
  2. package/dist/lib/browser/chunk-L2LSB6KV.mjs.map +7 -0
  3. package/dist/lib/browser/chunk-ZAOSIG3L.mjs +1814 -0
  4. package/dist/lib/browser/chunk-ZAOSIG3L.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +728 -2104
  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 +123 -0
  9. package/dist/lib/browser/testing/index.mjs.map +7 -0
  10. package/dist/lib/browser/types/index.mjs +3 -5
  11. package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs +20 -0
  12. package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs.map +7 -0
  13. package/dist/lib/node-esm/chunk-ZOFFI3JJ.mjs +1815 -0
  14. package/dist/lib/node-esm/chunk-ZOFFI3JJ.mjs.map +7 -0
  15. package/dist/lib/node-esm/index.mjs +728 -2104
  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 +124 -0
  19. package/dist/lib/node-esm/testing/index.mjs.map +7 -0
  20. package/dist/lib/node-esm/types/index.mjs +3 -5
  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 +4 -2
  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 +1 -1
  28. package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
  29. package/dist/types/src/components/Table/DynamicTable.stories.d.ts +1 -1
  30. package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
  31. package/dist/types/src/components/Table/{relations.stories.d.ts → Relations.stories.d.ts} +2 -2
  32. package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -0
  33. package/dist/types/src/components/Table/RowActionsMenu.d.ts +1 -1
  34. package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
  35. package/dist/types/src/components/Table/Table.d.ts +1 -0
  36. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  37. package/dist/types/src/components/Table/Table.stories.d.ts +1 -1
  38. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  39. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts +2 -1
  40. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts.map +1 -1
  41. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +2 -1
  42. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
  43. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +8 -7
  44. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  45. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +1 -1
  46. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  47. package/dist/types/src/components/TableCellEditor/extension.d.ts.map +1 -1
  48. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -1
  49. package/dist/types/src/components/testing.d.ts +3 -40
  50. package/dist/types/src/components/testing.d.ts.map +1 -1
  51. package/dist/types/src/hooks/index.d.ts +1 -0
  52. package/dist/types/src/hooks/index.d.ts.map +1 -1
  53. package/dist/types/src/hooks/useAddRow.d.ts +13 -0
  54. package/dist/types/src/hooks/useAddRow.d.ts.map +1 -0
  55. package/dist/types/src/hooks/useTableModel.d.ts +8 -7
  56. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  57. package/dist/types/src/index.d.ts +1 -2
  58. package/dist/types/src/index.d.ts.map +1 -1
  59. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  60. package/dist/types/src/model/selection-model.d.ts +2 -1
  61. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  62. package/dist/types/src/model/table-model.d.ts +58 -21
  63. package/dist/types/src/model/table-model.d.ts.map +1 -1
  64. package/dist/types/src/model/table-presentation.d.ts +10 -1
  65. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  66. package/dist/types/src/model/table-sorting.d.ts +4 -4
  67. package/dist/types/src/model/table-sorting.d.ts.map +1 -1
  68. package/dist/types/src/playwright/playwright.config.d.ts +3 -0
  69. package/dist/types/src/playwright/playwright.config.d.ts.map +1 -0
  70. package/dist/types/src/testing/index.d.ts +2 -0
  71. package/dist/types/src/testing/index.d.ts.map +1 -0
  72. package/dist/types/src/testing/useTestTableModel.d.ts +41 -0
  73. package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -0
  74. package/dist/types/src/translations.d.ts +5 -6
  75. package/dist/types/src/translations.d.ts.map +1 -1
  76. package/dist/types/src/types/index.d.ts +1 -1
  77. package/dist/types/src/types/index.d.ts.map +1 -1
  78. package/dist/types/src/types/schema.d.ts +8 -0
  79. package/dist/types/src/types/schema.d.ts.map +1 -0
  80. package/dist/types/src/util/dynamic-table.d.ts +3 -4
  81. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  82. package/dist/types/src/util/table-buttons.d.ts +15 -1
  83. package/dist/types/src/util/table-buttons.d.ts.map +1 -1
  84. package/dist/types/src/util/table-controls.d.ts.map +1 -1
  85. package/dist/types/src/util/table.d.ts +9 -11
  86. package/dist/types/src/util/table.d.ts.map +1 -1
  87. package/dist/types/tsconfig.tsbuildinfo +1 -1
  88. package/package.json +52 -40
  89. package/src/components/Table/ColumnActionsMenu.tsx +1 -1
  90. package/src/components/Table/ColumnSettings.tsx +15 -11
  91. package/src/components/Table/CreateRefPanel.tsx +6 -4
  92. package/src/components/Table/DynamicTable.stories.tsx +4 -3
  93. package/src/components/Table/DynamicTable.tsx +17 -10
  94. package/src/components/Table/{relations.stories.tsx → Relations.stories.tsx} +37 -37
  95. package/src/components/Table/RowActionsMenu.tsx +3 -3
  96. package/src/components/Table/Table.stories.tsx +109 -148
  97. package/src/components/Table/Table.tsx +95 -66
  98. package/src/components/TableCellEditor/CellValidationMessage.tsx +7 -2
  99. package/src/components/TableCellEditor/FormCellEditor.tsx +30 -21
  100. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +27 -30
  101. package/src/components/TableCellEditor/TableCellEditor.tsx +88 -34
  102. package/src/components/TableCellEditor/extension.ts +2 -2
  103. package/src/components/TableToolbar/TableToolbar.tsx +2 -2
  104. package/src/components/testing.ts +7 -24
  105. package/src/hooks/index.ts +1 -0
  106. package/src/hooks/useAddRow.ts +36 -0
  107. package/src/hooks/useTableModel.ts +21 -17
  108. package/src/index.ts +1 -2
  109. package/src/model/modal-controller.ts +1 -1
  110. package/src/model/selection-model.ts +6 -3
  111. package/src/model/table-model.test.ts +8 -8
  112. package/src/model/table-model.ts +278 -86
  113. package/src/model/table-presentation.test.ts +8 -8
  114. package/src/model/table-presentation.ts +240 -135
  115. package/src/model/table-sorting.ts +11 -11
  116. package/src/playwright/playwright.config.ts +17 -0
  117. package/src/playwright/smoke.spec.ts +113 -20
  118. package/src/testing/index.ts +5 -0
  119. package/src/testing/useTestTableModel.ts +123 -0
  120. package/src/translations.ts +7 -5
  121. package/src/types/index.ts +2 -2
  122. package/src/types/schema.ts +16 -0
  123. package/src/util/dynamic-table.ts +20 -18
  124. package/src/util/schema.ts +1 -1
  125. package/src/util/table-buttons.ts +26 -2
  126. package/src/util/table-controls.ts +4 -1
  127. package/src/util/table.ts +37 -111
  128. package/dist/lib/browser/chunk-TL7CKQQV.mjs +0 -27
  129. package/dist/lib/browser/chunk-TL7CKQQV.mjs.map +0 -7
  130. package/dist/lib/node/chunk-O2VPBNS5.cjs +0 -50
  131. package/dist/lib/node/chunk-O2VPBNS5.cjs.map +0 -7
  132. package/dist/lib/node/index.cjs +0 -2739
  133. package/dist/lib/node/index.cjs.map +0 -7
  134. package/dist/lib/node/meta.json +0 -1
  135. package/dist/lib/node/types/index.cjs +0 -31
  136. package/dist/lib/node/types/index.cjs.map +0 -7
  137. package/dist/lib/node-esm/chunk-GBT6GD4G.mjs +0 -29
  138. package/dist/lib/node-esm/chunk-GBT6GD4G.mjs.map +0 -7
  139. package/dist/types/src/components/Table/relations.stories.d.ts.map +0 -1
  140. package/dist/types/src/types/table.d.ts +0 -56
  141. package/dist/types/src/types/table.d.ts.map +0 -1
  142. package/src/playwright/playwright.config.cts +0 -18
  143. package/src/types/table.ts +0 -27
@@ -4,11 +4,13 @@
4
4
 
5
5
  import { expect, test } from '@playwright/test';
6
6
 
7
+ import { type DxGrid } from '@dxos/lit-grid';
7
8
  import { setupPage, storybookUrl } from '@dxos/test-utils/playwright';
8
9
 
9
10
  import { TableManager } from './TableManager';
10
11
 
11
- const storyUrl = storybookUrl('ui-react-ui-table-table--default');
12
+ const storyUrl = storybookUrl('ui-react-ui-table-table--default', 9004);
13
+ const relationsStoryUrl = storybookUrl('ui-react-ui-table-relations--default', 9004);
12
14
 
13
15
  // NOTE(ZaymonFC): This test suite relies on the faker seed being set to 0 in the story.
14
16
  test.describe('Table', () => {
@@ -28,12 +30,12 @@ test.describe('Table', () => {
28
30
 
29
31
  await table.grid.ready();
30
32
  await table.sortColumn(0, 'descending');
31
- await expect(table.grid.cell(0, 0, 'grid')).toHaveText('Uwe Øvergård');
32
- await expect(table.grid.cell(0, 9, 'grid')).toContainText('Anita');
33
+ await expect(table.grid.cell(0, 0, 'grid')).toHaveText('Sit.');
34
+ await expect(table.grid.cell(0, 9, 'grid')).toContainText('Aut.');
33
35
 
34
36
  await table.sortColumn(0, 'ascending');
35
- await expect(table.grid.cell(0, 0, 'grid')).toContainText('Anita');
36
- await expect(table.grid.cell(0, 9, 'grid')).toHaveText('Uwe Øvergård');
37
+ await expect(table.grid.cell(0, 0, 'grid')).toContainText('Aut.');
38
+ await expect(table.grid.cell(0, 9, 'grid')).toHaveText('Sit.');
37
39
  await page.close();
38
40
  });
39
41
 
@@ -65,7 +67,7 @@ test.describe('Table', () => {
65
67
 
66
68
  await table.grid.ready();
67
69
  await table.deleteRow(0);
68
- await expect(page.getByRole('gridcell', { name: 'Anita Mayer' })).toHaveCount(0);
70
+ await expect(page.getByRole('gridcell', { name: 'Sapiente.' })).toHaveCount(0);
69
71
  await page.close();
70
72
  });
71
73
 
@@ -80,8 +82,8 @@ test.describe('Table', () => {
80
82
  // Delete action affects all selected rows.
81
83
  await table.deleteRow(0);
82
84
 
83
- await expect(page.getByRole('gridcell', { name: 'Anita Mayer' })).toHaveCount(0);
84
- await expect(page.getByRole('gridcell', { name: 'Phonthip Sigurjónsson' })).toHaveCount(0);
85
+ await expect(page.getByRole('gridcell', { name: 'Sapiente.' })).toHaveCount(0);
86
+ await expect(page.getByRole('gridcell', { name: 'Beatae.' })).toHaveCount(0);
85
87
  await expect(table.grid.cellsWithinPlane('grid')).toHaveCount(0);
86
88
  await page.close();
87
89
  });
@@ -112,7 +114,6 @@ test.describe('Table', () => {
112
114
  });
113
115
 
114
116
  // Rest of add column test remains the same as it's a more complex flow.
115
- // TODO(ZaymonFC): Restore this after fixing format selection.
116
117
  test('add column', async ({ browser, browserName }) => {
117
118
  test.skip(browserName === 'webkit');
118
119
  const { page } = await setupPage(browser, { url: storyUrl });
@@ -139,19 +140,21 @@ test.describe('Table', () => {
139
140
  await table.grid.panByWheel(10000, 0);
140
141
 
141
142
  await table.grid.cell(4, 0, 'grid').click();
142
- await page.keyboard.press('A');
143
- await page.keyboard.press('N');
144
- await page.getByRole('option', { name: 'Anita Mayer' }).click();
143
+ // TODO(wittjosiah): Surprisingly long delay needed here.
144
+ await page.keyboard.press('A', { delay: 1_000 });
145
+ await page.keyboard.press('u', { delay: 1_000 });
146
+ await page.getByRole('option', { name: 'Aut.' }).click();
145
147
 
146
148
  // Assert that the value is shown in the cell.
147
- await expect(page.getByRole('gridcell', { name: 'Anita Mayer' }).first()).toBeVisible();
149
+ await expect(page.getByRole('gridcell', { name: 'Aut.' }).first()).toBeVisible();
148
150
 
149
151
  // Create new object.
150
152
  await table.grid.cell(4, 1, 'grid').click();
151
- await page.keyboard.type('t');
152
- await page.keyboard.type('e');
153
- await page.keyboard.type('s');
154
- await page.keyboard.type('t');
153
+ // TODO(wittjosiah): Surprisingly long delay needed here.
154
+ await page.keyboard.type('t', { delay: 1_000 });
155
+ await page.keyboard.type('e', { delay: 1_000 });
156
+ await page.keyboard.type('s', { delay: 1_000 });
157
+ await page.keyboard.type('t', { delay: 1_000 });
155
158
  await page.getByText('Create new object', { exact: false }).click();
156
159
  await page.getByTestId('save-button').click();
157
160
 
@@ -172,13 +175,103 @@ test.describe('Table', () => {
172
175
  await page.getByTestId('table-switch').nth(7).click();
173
176
 
174
177
  // Test that checks are durable in the data model by sorting.
175
- await table.sortColumn(3, 'descending');
178
+ await table.sortColumn(1, 'descending');
176
179
 
177
180
  // Assert the first two switch checkboxes are checked.
178
181
  await expect(page.getByTestId('table-switch').first()).toBeChecked();
179
182
  await expect(page.getByTestId('table-switch').nth(1)).toBeChecked();
180
- await expect(table.grid.cell(0, 0, 'grid')).toHaveText('Anita Mayer');
181
- await expect(table.grid.cell(0, 1, 'grid')).toHaveText('Uwe Øvergård');
183
+ await expect(table.grid.cell(0, 0, 'grid')).toHaveText('Sapiente.');
184
+ await expect(table.grid.cell(0, 1, 'grid')).toHaveText('Beatae.');
185
+
186
+ await page.close();
187
+ });
188
+
189
+ test('extant relations work as expected', async ({ browser, browserName }) => {
190
+ test.skip(browserName === 'webkit');
191
+ const { page } = await setupPage(browser, { url: relationsStoryUrl });
192
+
193
+ // Wait for the page to load
194
+ await page.locator('dx-grid > .dx-grid').nth(1).waitFor({ state: 'visible' });
195
+
196
+ // Find the dx-grid element for the contactModel (second table)
197
+ // The contactModel is used in the second Table.Main component in the story
198
+ const dxGrid = page.locator('dx-grid').nth(1);
199
+ await dxGrid.waitFor({ state: 'visible' });
200
+
201
+ // Scroll to the last column (column 8)
202
+ await dxGrid.evaluate(async (dxGridElement: DxGrid) => {
203
+ dxGridElement.scrollToColumn(8);
204
+ });
205
+
206
+ // Click on the cell at aria-rowindex=0 aria-colindex=8 to focus it
207
+ const targetCell = dxGrid.locator('[data-dx-grid-plane="grid"] [aria-rowindex="0"][aria-colindex="8"]');
208
+ await targetCell.click();
209
+
210
+ // Click again to engage edit mode
211
+ await page.keyboard.press('Enter');
212
+ await page.getByTestId('grid.cell-editor').waitFor({ state: 'visible' });
213
+
214
+ // Type the first few letters of an org name.
215
+ const orgName =
216
+ (await page
217
+ .locator('dx-grid')
218
+ .nth(0)
219
+ .locator('[data-dx-grid-plane="grid"] [aria-rowindex="0"][aria-colindex="0"] .dx-grid__cell__content')
220
+ .textContent()) ?? 'never';
221
+ await page.keyboard.type(orgName.substring(0, 4), { delay: 500 });
222
+
223
+ // Assert that there is an element with aria-selected on the page
224
+ await expect(page.locator('[role="option"][aria-selected]')).toBeVisible();
225
+
226
+ // Type the enter key
227
+ await page.keyboard.press('Enter');
228
+
229
+ // Assert that the cell element has the org name
230
+ await expect(targetCell).toHaveText(orgName);
231
+
232
+ await page.close();
233
+ });
234
+
235
+ test('new relations work as expected', async ({ browser, browserName }) => {
236
+ test.skip(browserName === 'webkit');
237
+ const { page } = await setupPage(browser, { url: relationsStoryUrl });
238
+
239
+ // Wait for the page to load
240
+ await page.locator('dx-grid > .dx-grid').nth(1).waitFor({ state: 'visible' });
241
+
242
+ // Find the dx-grid element for the contactModel (second table)
243
+ // The contactModel is used in the second Table.Main component in the story
244
+ const dxGrid = page.locator('dx-grid').nth(1);
245
+ await dxGrid.waitFor({ state: 'visible' });
246
+
247
+ // Scroll to the last column (column 8)
248
+ await dxGrid.evaluate(async (dxGridElement: DxGrid) => {
249
+ dxGridElement.scrollToColumn(8);
250
+ });
251
+
252
+ // Click on the cell at aria-rowindex=0 aria-colindex=8 to focus it
253
+ const targetCell = dxGrid.locator('[data-dx-grid-plane="grid"] [aria-rowindex="0"][aria-colindex="8"]');
254
+ await targetCell.click();
255
+
256
+ // Click again to engage edit mode
257
+ await page.keyboard.press('Enter');
258
+ await page.getByTestId('grid.cell-editor').waitFor({ state: 'visible' });
259
+
260
+ // Type the first few letters of an org name.
261
+ const orgName = 'Sally';
262
+ await page.keyboard.type(orgName, { delay: 500 });
263
+
264
+ // Assert that there is an element with aria-selected on the page
265
+ await expect(page.locator('[role="option"][aria-selected]')).toBeVisible();
266
+
267
+ // Type the enter key
268
+ await page.keyboard.press('Enter');
269
+
270
+ // Click the save button in the popover
271
+ await page.getByTestId('save-button').click();
272
+
273
+ // Assert that the cell element has the org name
274
+ await expect(targetCell).toHaveText(orgName);
182
275
 
183
276
  await page.close();
184
277
  });
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './useTestTableModel';
@@ -0,0 +1,123 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { useCallback, useMemo, useRef } from 'react';
6
+
7
+ import { isMutable, toJsonSchema } from '@dxos/echo-schema';
8
+ import { useGlobalFilteredObjects } from '@dxos/plugin-search';
9
+ import { faker } from '@dxos/random';
10
+ import { useClient } from '@dxos/react-client';
11
+ import { Filter, useQuery, useSchema } from '@dxos/react-client/echo';
12
+ import { useClientProvider } from '@dxos/react-client/testing';
13
+ import { DataType, ProjectionModel } from '@dxos/schema';
14
+
15
+ import { type TableController } from '../components';
16
+ import { useAddRow, useTableModel } from '../hooks';
17
+ import { TablePresentation } from '../model';
18
+
19
+ faker.seed(0); // NOTE(ZaymonFC): Required for smoke tests.
20
+
21
+ /**
22
+ * Custom hook to create and manage a test table model for storybook demonstrations.
23
+ * Provides table data, schema, and handlers for table operations.
24
+ */
25
+ export const useTestTableModel = () => {
26
+ const client = useClient();
27
+ const { space } = useClientProvider();
28
+
29
+ const views = useQuery(space, Filter.type(DataType.View));
30
+ const view = useMemo(() => views.at(0), [views]);
31
+ const schema = useSchema(client, space, view?.query.typename);
32
+ const jsonSchema = useMemo(() => (schema ? toJsonSchema(schema) : undefined), [schema]);
33
+
34
+ const projection = useMemo(() => {
35
+ if (schema && view?.projection) {
36
+ return new ProjectionModel(toJsonSchema(schema), view.projection);
37
+ }
38
+ }, [schema, view?.projection]);
39
+
40
+ const features = useMemo(
41
+ () => ({
42
+ selection: { enabled: true, mode: 'multiple' as const },
43
+ dataEditable: true,
44
+ schemaEditable: schema && isMutable(schema),
45
+ }),
46
+ [schema],
47
+ );
48
+
49
+ const objects = useQuery(space, schema ? Filter.type(schema) : Filter.nothing());
50
+ const filteredObjects = useGlobalFilteredObjects(objects);
51
+
52
+ const tableRef = useRef<TableController>(null);
53
+ const handleCellUpdate = useCallback((cell: any) => {
54
+ tableRef.current?.update?.(cell);
55
+ }, []);
56
+
57
+ const handleRowOrderChange = useCallback(() => {
58
+ tableRef.current?.update?.();
59
+ }, []);
60
+
61
+ const addRow = useAddRow({ space, schema });
62
+
63
+ const handleDeleteRows = useCallback(
64
+ (_: number, objects: any[]) => {
65
+ for (const object of objects) {
66
+ space?.db.remove(object);
67
+ }
68
+ },
69
+ [space],
70
+ );
71
+
72
+ const handleDeleteColumn = useCallback(
73
+ (fieldId: string) => {
74
+ if (projection) {
75
+ projection.deleteFieldProjection(fieldId);
76
+ }
77
+ },
78
+ [projection],
79
+ );
80
+
81
+ const model = useTableModel({
82
+ view,
83
+ schema: jsonSchema,
84
+ projection,
85
+ features,
86
+ rows: filteredObjects,
87
+ onInsertRow: addRow,
88
+ onDeleteRows: handleDeleteRows,
89
+ onDeleteColumn: handleDeleteColumn,
90
+ onCellUpdate: handleCellUpdate,
91
+ onRowOrderChange: handleRowOrderChange,
92
+ });
93
+
94
+ const handleInsertRow = useCallback(() => {
95
+ model?.insertRow();
96
+ tableRef.current?.focusDraft?.();
97
+ }, [model, tableRef.current]);
98
+
99
+ const handleSaveView = useCallback(() => {
100
+ model?.saveView();
101
+ }, [model]);
102
+
103
+ const presentation = useMemo(() => {
104
+ if (model) {
105
+ return new TablePresentation(model);
106
+ }
107
+ }, [model]);
108
+
109
+ return {
110
+ schema,
111
+ view,
112
+ projection,
113
+ tableRef,
114
+ model,
115
+ presentation,
116
+ space,
117
+ client,
118
+ handleInsertRow,
119
+ handleSaveView,
120
+ handleDeleteRows,
121
+ handleDeleteColumn,
122
+ };
123
+ };
@@ -2,9 +2,11 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- export const translationKey = 'table';
5
+ import { type Resource } from '@dxos/react-ui';
6
6
 
7
- export default [
7
+ export const translationKey = 'react-ui-table';
8
+
9
+ export const translations = [
8
10
  {
9
11
  'en-US': {
10
12
  [translationKey]: {
@@ -12,8 +14,8 @@ export default [
12
14
  'create object label': 'Create table',
13
15
  'table name placeholder': 'Table name',
14
16
  'settings title': 'Table settings',
15
- 'table schema label': 'Schema',
16
- 'new schema': 'New schema',
17
+ 'table schema label': 'Record type',
18
+ 'new schema': 'New record type',
17
19
  'continue label': 'Continue',
18
20
  'create stack section label': 'Create table',
19
21
  'add row label': 'Add row',
@@ -32,4 +34,4 @@ export default [
32
34
  },
33
35
  },
34
36
  },
35
- ] as const;
37
+ ] as const satisfies Resource[];
@@ -1,5 +1,5 @@
1
1
  //
2
- // Copyright 2024 DXOS.org
2
+ // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- export * from './table';
5
+ export * from './schema';
@@ -0,0 +1,16 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { Schema } from 'effect';
6
+
7
+ import { Type } from '@dxos/echo';
8
+ import { JsonPath } from '@dxos/effect';
9
+
10
+ export const TableView = Schema.Struct({
11
+ sizes: Schema.Record({
12
+ key: JsonPath,
13
+ value: Schema.Number,
14
+ }).pipe(Schema.mutable),
15
+ }).pipe(Type.Obj({ typename: 'dxos.org/type/TableView', version: '0.1.0' }));
16
+ export type TableView = Schema.Schema.Type<typeof TableView>;
@@ -4,18 +4,18 @@
4
4
 
5
5
  import { type Schema } from 'effect';
6
6
 
7
- import { Obj, Ref } from '@dxos/echo';
7
+ import { Obj } from '@dxos/echo';
8
8
  import { getTypename, toJsonSchema } from '@dxos/echo-schema';
9
9
  import type { JsonSchemaType, SortDirectionType } from '@dxos/echo-schema';
10
10
  import {
11
+ type DataType,
12
+ ProjectionModel,
13
+ type SchemaPropertyDefinition,
11
14
  createView,
12
15
  getSchemaFromPropertyDefinitions,
13
- ViewProjection,
14
- type ViewType,
15
- type SchemaPropertyDefinition,
16
16
  } from '@dxos/schema';
17
17
 
18
- import { TableType } from '../types';
18
+ import { TableView } from '../types';
19
19
 
20
20
  // TODO(ZaymonFC): Upstream these extra fields to SchemaPropertyDefinition to enhance schema-tools schema creation.
21
21
  type PropertyDisplayProps = {
@@ -61,32 +61,34 @@ export const makeDynamicTable = ({
61
61
  typename: string;
62
62
  jsonSchema: JsonSchemaType;
63
63
  properties?: TablePropertyDefinition[];
64
- }): { table: TableType; projection: ViewProjection } => {
64
+ }): { projection: ProjectionModel; view: DataType.View } => {
65
+ const table = Obj.make(TableView, { sizes: {} });
65
66
  const view = createView({
66
- name: 'dynamic-table',
67
67
  typename,
68
68
  jsonSchema,
69
+ presentation: table,
69
70
  ...(properties && { fields: properties.map((property) => property.name) }),
70
71
  });
71
72
 
72
- const table = Obj.make(TableType, { name: 'dynamic-table', view: Ref.make(view) });
73
- const projection = new ViewProjection(jsonSchema, view);
74
- if (properties && view.fields) {
75
- setProperties(view, projection, properties);
73
+ const projection = new ProjectionModel(jsonSchema, view.projection);
74
+ if (properties && projection.fields) {
75
+ setProperties(view, projection, table, properties);
76
76
  }
77
77
 
78
- return {
79
- table,
80
- projection,
81
- };
78
+ return { projection, view };
82
79
  };
83
80
 
84
- const setProperties = (view: ViewType, projection: ViewProjection, properties: TablePropertyDefinition[]) => {
81
+ const setProperties = (
82
+ view: DataType.View,
83
+ projection: ProjectionModel,
84
+ table: TableView,
85
+ properties: TablePropertyDefinition[],
86
+ ) => {
85
87
  for (const property of properties) {
86
- const field = view.fields.find((field) => field.path === property.name);
88
+ const field = projection.fields.find((field) => field.path === property.name);
87
89
  if (field) {
88
90
  if (property.size !== undefined) {
89
- field.size = property.size;
91
+ table.sizes[field.path] = property.size;
90
92
  }
91
93
 
92
94
  if (property.title !== undefined) {
@@ -3,7 +3,7 @@
3
3
  //
4
4
 
5
5
  import { Schema } from 'effect';
6
- import { isTypeLiteral, TypeLiteral } from 'effect/SchemaAST';
6
+ import { TypeLiteral, isTypeLiteral } from 'effect/SchemaAST';
7
7
 
8
8
  /**
9
9
  * Creates a narrowed schema from an original schema that only includes
@@ -2,13 +2,14 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- export type TableButton = 'columnSettings' | 'rowMenu' | 'newColumn' | 'referencedCell' | 'sort';
5
+ export type TableButton = 'columnSettings' | 'rowMenu' | 'newColumn' | 'referencedCell' | 'sort' | 'saveDraftRow';
6
6
 
7
7
  export const BUTTON_IDENTIFIERS: { [K in TableButton]: string } = {
8
8
  columnSettings: 'data-table-column-settings-button',
9
9
  newColumn: 'data-table-new-column-button',
10
10
  referencedCell: 'data-table-ref-cell-button',
11
11
  rowMenu: 'data-table-row-menu-button',
12
+ saveDraftRow: 'data-table-save-draft-row-button',
12
13
  sort: 'data-table-sort-button',
13
14
  } as const;
14
15
 
@@ -16,6 +17,7 @@ type ButtonData =
16
17
  | { type: 'columnSettings'; fieldId: string }
17
18
  | { type: 'newColumn'; disabled?: boolean }
18
19
  | { type: 'rowMenu'; rowIndex: number }
20
+ | { type: 'saveDraftRow'; rowIndex: number; disabled?: boolean }
19
21
  | { type: 'sort'; fieldId: string; direction?: 'asc' | 'desc' };
20
22
 
21
23
  const createButton = ({
@@ -38,7 +40,7 @@ const createButton = ({
38
40
  .join(' ');
39
41
 
40
42
  const positionClass = type === 'primary' ? 'inline-end-2' : 'inline-end-9';
41
- return `<button ${attr} data-testid="${testId}" class="dx-button is-6 pli-0.5 min-bs-0 absolute inset-block-1 ${positionClass}" ${dataAttrs} ${disabled ? 'disabled' : ''}><svg data-size="4"><use href="/icons.svg#${icon}"/></svg></button>`;
43
+ return `<button ${attr} data-testid="${testId}" class="dx-button is-6 pli-0.5 min-bs-0 absolute inset-block-1 ${positionClass}" ${dataAttrs} ${disabled ? 'disabled' : ''} data-dx-grid-action="accessory"><svg data-size="4"><use href="/icons.svg#${icon}"/></svg></button>`;
42
44
  };
43
45
 
44
46
  const addColumnButton = {
@@ -118,9 +120,31 @@ const sortButton = {
118
120
  }),
119
121
  } as const;
120
122
 
123
+ const saveDraftRowButton = {
124
+ attr: BUTTON_IDENTIFIERS.saveDraftRow,
125
+ icon: 'ph--floppy-disk--regular',
126
+ render: ({ rowIndex, disabled }: Omit<Extract<ButtonData, { type: 'saveDraftRow' }>, 'type'>) => {
127
+ return createButton({
128
+ attr: BUTTON_IDENTIFIERS.saveDraftRow,
129
+ icon: saveDraftRowButton.icon,
130
+ disabled,
131
+ data: {
132
+ 'data-row-index': rowIndex.toString(),
133
+ 'data-variant': 'primary',
134
+ },
135
+ testId: 'table-save-draft-row-button',
136
+ });
137
+ },
138
+ getData: (el: HTMLElement): Extract<ButtonData, { type: 'saveDraftRow' }> => ({
139
+ type: 'saveDraftRow',
140
+ rowIndex: Number(el.getAttribute('data-row-index')!),
141
+ }),
142
+ } as const;
143
+
121
144
  export const tableButtons = {
122
145
  addColumn: addColumnButton,
123
146
  columnSettings: columnSettingsButton,
124
147
  rowMenu: rowMenuButton,
148
+ saveDraftRow: saveDraftRowButton,
125
149
  sort: sortButton,
126
150
  } as const;
@@ -3,6 +3,8 @@
3
3
  //
4
4
 
5
5
  // Core types.
6
+ import { accessoryHandlesPointerdownAttrs } from '@dxos/lit-grid';
7
+
6
8
  export type TableControl = 'checkbox' | 'switch';
7
9
 
8
10
  export type ControlData =
@@ -41,7 +43,7 @@ const renderInput = (
41
43
  preventToggle = false,
42
44
  testId: string,
43
45
  ) => {
44
- return `<input type="checkbox" class="${baseClass}" ${renderAttributes(attrs)} ${checked ? 'checked' : ''} ${preventToggle ? 'onclick="return false"' : ''} ${disabled ? 'disabled' : ''} data-testid="${testId}"/>`;
46
+ return `<input type="checkbox" class="${baseClass}" ${renderAttributes(attrs)} ${checked ? 'checked' : ''} ${preventToggle ? 'onclick="return false"' : ''} ${disabled ? 'disabled' : ''} data-testid="${testId}" data-dx-grid-action="accessory"/>`;
45
47
  };
46
48
 
47
49
  export const CheckboxStory = ({
@@ -64,6 +66,7 @@ export const SwitchStory = ({ colIndex, rowIndex, checked = false, disabled = fa
64
66
  [CONTROL_IDENTIFIERS.switch]: '',
65
67
  'data-row-index': rowIndex.toString(),
66
68
  'data-col-index': colIndex.toString(),
69
+ ...accessoryHandlesPointerdownAttrs,
67
70
  };
68
71
 
69
72
  return renderInput(BASE_CLASSES.switch, attrs, checked, disabled, true, 'table-switch');