@dxos/react-ui-table 0.8.4-main.f5c0578 → 0.8.4-main.fcfe5033a5

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 (162) hide show
  1. package/dist/lib/browser/chunk-BF54SSNL.mjs +70 -0
  2. package/dist/lib/browser/chunk-BF54SSNL.mjs.map +7 -0
  3. package/dist/lib/browser/{chunk-ZAOSIG3L.mjs → chunk-F7Y374KL.mjs} +739 -802
  4. package/dist/lib/browser/chunk-F7Y374KL.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +1208 -1158
  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 +34 -43
  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-ZOFFI3JJ.mjs → chunk-CCJ4LOVM.mjs} +739 -801
  12. package/dist/lib/node-esm/chunk-CCJ4LOVM.mjs.map +7 -0
  13. package/dist/lib/node-esm/chunk-SIRDIPHZ.mjs +71 -0
  14. package/dist/lib/node-esm/chunk-SIRDIPHZ.mjs.map +7 -0
  15. package/dist/lib/node-esm/index.mjs +1208 -1158
  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 +34 -43
  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 -2
  22. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts.map +1 -1
  23. package/dist/types/src/components/Table/ColumnSettings.d.ts +2 -2
  24. package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
  25. package/dist/types/src/components/Table/DynamicTable.d.ts +7 -7
  26. package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
  27. package/dist/types/src/components/Table/DynamicTable.stories.d.ts +35 -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 +106 -5
  30. package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -1
  31. package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
  32. package/dist/types/src/components/Table/Table.d.ts +21 -19
  33. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  34. package/dist/types/src/components/Table/Table.stories.d.ts +108 -5
  35. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  36. package/dist/types/src/components/Table/TableContent.d.ts +17 -0
  37. package/dist/types/src/components/Table/TableContent.d.ts.map +1 -0
  38. package/dist/types/src/components/Table/TableToolbar.d.ts +21 -0
  39. package/dist/types/src/components/Table/TableToolbar.d.ts.map +1 -0
  40. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts +1 -1
  41. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +13 -9
  42. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
  43. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +7 -14
  44. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  45. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +38 -5
  46. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  47. package/dist/types/src/components/TableCellEditor/index.d.ts +1 -0
  48. package/dist/types/src/components/TableCellEditor/index.d.ts.map +1 -1
  49. package/dist/types/src/components/index.d.ts +0 -1
  50. package/dist/types/src/components/index.d.ts.map +1 -1
  51. package/dist/types/src/components/testing.d.ts +23 -16
  52. package/dist/types/src/components/testing.d.ts.map +1 -1
  53. package/dist/types/src/hooks/index.d.ts +2 -1
  54. package/dist/types/src/hooks/index.d.ts.map +1 -1
  55. package/dist/types/src/hooks/useAddRow.d.ts +6 -6
  56. package/dist/types/src/hooks/useAddRow.d.ts.map +1 -1
  57. package/dist/types/src/hooks/useProjectionModel.d.ts +6 -0
  58. package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -0
  59. package/dist/types/src/hooks/useTableModel.d.ts +9 -9
  60. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  61. package/dist/types/src/model/modal-controller.d.ts +5 -8
  62. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  63. package/dist/types/src/model/selection-model.d.ts +17 -8
  64. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  65. package/dist/types/src/model/table-model.d.ts +110 -29
  66. package/dist/types/src/model/table-model.d.ts.map +1 -1
  67. package/dist/types/src/model/table-presentation.d.ts +4 -2
  68. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  69. package/dist/types/src/playwright/TableManager.d.ts +2 -1
  70. package/dist/types/src/playwright/TableManager.d.ts.map +1 -1
  71. package/dist/types/src/testing/useTestTableModel.d.ts +18 -34
  72. package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -1
  73. package/dist/types/src/translations.d.ts +21 -23
  74. package/dist/types/src/translations.d.ts.map +1 -1
  75. package/dist/types/src/types/Table.d.ts +26 -0
  76. package/dist/types/src/types/Table.d.ts.map +1 -0
  77. package/dist/types/src/types/index.d.ts +1 -1
  78. package/dist/types/src/types/index.d.ts.map +1 -1
  79. package/dist/types/src/util/dynamic-table.d.ts +14 -12
  80. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  81. package/dist/types/src/util/dynamic-table.test.d.ts +2 -0
  82. package/dist/types/src/util/dynamic-table.test.d.ts.map +1 -0
  83. package/dist/types/src/util/index.d.ts +2 -1
  84. package/dist/types/src/util/index.d.ts.map +1 -1
  85. package/dist/types/src/util/query-ast.d.ts +7 -0
  86. package/dist/types/src/util/query-ast.d.ts.map +1 -0
  87. package/dist/types/src/util/schema.d.ts +1 -1
  88. package/dist/types/src/util/schema.d.ts.map +1 -1
  89. package/dist/types/src/util/sort.d.ts +12 -0
  90. package/dist/types/src/util/sort.d.ts.map +1 -0
  91. package/dist/types/src/util/table-buttons.d.ts.map +1 -1
  92. package/dist/types/tsconfig.tsbuildinfo +1 -1
  93. package/package.json +58 -57
  94. package/src/components/Table/ColumnActionsMenu.tsx +15 -11
  95. package/src/components/Table/ColumnSettings.tsx +8 -9
  96. package/src/components/Table/DynamicTable.stories.tsx +36 -34
  97. package/src/components/Table/DynamicTable.tsx +33 -36
  98. package/src/components/Table/Relations.stories.tsx +180 -59
  99. package/src/components/Table/RowActionsMenu.tsx +24 -15
  100. package/src/components/Table/Table.stories.tsx +152 -129
  101. package/src/components/Table/Table.tsx +46 -391
  102. package/src/components/Table/TableContent.tsx +421 -0
  103. package/src/components/Table/TableToolbar.tsx +111 -0
  104. package/src/components/TableCellEditor/CellValidationMessage.tsx +5 -5
  105. package/src/components/TableCellEditor/FormCellEditor.tsx +134 -40
  106. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +35 -72
  107. package/src/components/TableCellEditor/TableCellEditor.tsx +80 -224
  108. package/src/components/TableCellEditor/index.ts +1 -0
  109. package/src/components/index.ts +0 -1
  110. package/src/components/testing.ts +44 -18
  111. package/src/hooks/index.ts +2 -1
  112. package/src/hooks/useAddRow.ts +14 -14
  113. package/src/hooks/useProjectionModel.ts +39 -0
  114. package/src/hooks/useTableModel.ts +36 -34
  115. package/src/model/modal-controller.ts +23 -40
  116. package/src/model/selection-model.ts +75 -44
  117. package/src/model/table-model.test.ts +91 -56
  118. package/src/model/table-model.ts +447 -172
  119. package/src/model/table-presentation.test.ts +68 -58
  120. package/src/model/table-presentation.ts +97 -73
  121. package/src/playwright/TableManager.ts +13 -6
  122. package/src/playwright/playwright.config.ts +1 -1
  123. package/src/playwright/smoke.spec.ts +92 -46
  124. package/src/testing/useTestTableModel.ts +50 -40
  125. package/src/translations.ts +20 -22
  126. package/src/types/Table.ts +79 -0
  127. package/src/types/index.ts +1 -1
  128. package/src/util/dynamic-table.test.ts +70 -0
  129. package/src/util/dynamic-table.ts +45 -32
  130. package/src/util/index.ts +2 -1
  131. package/src/util/query-ast.ts +22 -0
  132. package/src/util/schema.ts +4 -4
  133. package/src/util/sort.ts +90 -0
  134. package/src/util/table-buttons.ts +3 -3
  135. package/src/util/table-controls.ts +2 -2
  136. package/dist/lib/browser/chunk-L2LSB6KV.mjs +0 -18
  137. package/dist/lib/browser/chunk-L2LSB6KV.mjs.map +0 -7
  138. package/dist/lib/browser/chunk-ZAOSIG3L.mjs.map +0 -7
  139. package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs +0 -20
  140. package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs.map +0 -7
  141. package/dist/lib/node-esm/chunk-ZOFFI3JJ.mjs.map +0 -7
  142. package/dist/types/src/components/Table/CreateRefPanel.d.ts +0 -9
  143. package/dist/types/src/components/Table/CreateRefPanel.d.ts.map +0 -1
  144. package/dist/types/src/components/TableCellEditor/extension.d.ts +0 -13
  145. package/dist/types/src/components/TableCellEditor/extension.d.ts.map +0 -1
  146. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +0 -21
  147. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +0 -1
  148. package/dist/types/src/components/TableToolbar/index.d.ts +0 -2
  149. package/dist/types/src/components/TableToolbar/index.d.ts.map +0 -1
  150. package/dist/types/src/model/table-sorting.d.ts +0 -53
  151. package/dist/types/src/model/table-sorting.d.ts.map +0 -1
  152. package/dist/types/src/types/schema.d.ts +0 -8
  153. package/dist/types/src/types/schema.d.ts.map +0 -1
  154. package/dist/types/src/util/table.d.ts +0 -12
  155. package/dist/types/src/util/table.d.ts.map +0 -1
  156. package/src/components/Table/CreateRefPanel.tsx +0 -86
  157. package/src/components/TableCellEditor/extension.ts +0 -61
  158. package/src/components/TableToolbar/TableToolbar.tsx +0 -102
  159. package/src/components/TableToolbar/index.ts +0 -5
  160. package/src/model/table-sorting.ts +0 -216
  161. package/src/types/schema.ts +0 -16
  162. package/src/util/table.ts +0 -53
@@ -5,6 +5,7 @@
5
5
  import { expect, test } from '@playwright/test';
6
6
 
7
7
  import { type DxGrid } from '@dxos/lit-grid';
8
+ import { random } from '@dxos/random';
8
9
  import { setupPage, storybookUrl } from '@dxos/test-utils/playwright';
9
10
 
10
11
  import { TableManager } from './TableManager';
@@ -12,10 +13,11 @@ import { TableManager } from './TableManager';
12
13
  const storyUrl = storybookUrl('ui-react-ui-table-table--default', 9004);
13
14
  const relationsStoryUrl = storybookUrl('ui-react-ui-table-relations--default', 9004);
14
15
 
15
- // NOTE(ZaymonFC): This test suite relies on the faker seed being set to 0 in the story.
16
+ // NOTE(ZaymonFC): This test suite relies on the random seed being set to 0 in the story.
16
17
  test.describe('Table', () => {
17
18
  test('Loads', async ({ browser, browserName }) => {
18
19
  test.skip(browserName === 'webkit');
20
+ test.skip(browserName === 'firefox');
19
21
  const { page } = await setupPage(browser, { url: storyUrl });
20
22
  const table = new TableManager(page);
21
23
 
@@ -25,6 +27,7 @@ test.describe('Table', () => {
25
27
 
26
28
  test('sort', async ({ browser, browserName }) => {
27
29
  test.skip(browserName === 'webkit');
30
+ test.skip(browserName === 'firefox');
28
31
  const { page } = await setupPage(browser, { url: storyUrl });
29
32
  const table = new TableManager(page);
30
33
 
@@ -41,6 +44,7 @@ test.describe('Table', () => {
41
44
 
42
45
  test('selection', async ({ browser, browserName }) => {
43
46
  test.skip(browserName === 'webkit');
47
+ test.skip(browserName === 'firefox');
44
48
  const { page } = await setupPage(browser, { url: storyUrl });
45
49
  const table = new TableManager(page);
46
50
 
@@ -62,10 +66,12 @@ test.describe('Table', () => {
62
66
 
63
67
  test('delete row', async ({ browser, browserName }) => {
64
68
  test.skip(browserName === 'webkit');
69
+ test.skip(browserName === 'firefox');
65
70
  const { page } = await setupPage(browser, { url: storyUrl });
66
71
  const table = new TableManager(page);
67
72
 
68
73
  await table.grid.ready();
74
+ await expect(page.getByRole('gridcell', { name: 'Sapiente.' })).toHaveCount(1);
69
75
  await table.deleteRow(0);
70
76
  await expect(page.getByRole('gridcell', { name: 'Sapiente.' })).toHaveCount(0);
71
77
  await page.close();
@@ -73,6 +79,7 @@ test.describe('Table', () => {
73
79
 
74
80
  test('delete row--select all', async ({ browser, browserName }) => {
75
81
  test.skip(browserName === 'webkit');
82
+ test.skip(browserName === 'firefox');
76
83
  const { page } = await setupPage(browser, { url: storyUrl });
77
84
  const table = new TableManager(page);
78
85
 
@@ -82,7 +89,7 @@ test.describe('Table', () => {
82
89
  // Delete action affects all selected rows.
83
90
  await table.deleteRow(0);
84
91
 
85
- await expect(page.getByRole('gridcell', { name: 'Sapiente.' })).toHaveCount(0);
92
+ await expect(page.getByRole('gridcell', { name: 'Aut.' })).toHaveCount(0);
86
93
  await expect(page.getByRole('gridcell', { name: 'Beatae.' })).toHaveCount(0);
87
94
  await expect(table.grid.cellsWithinPlane('grid')).toHaveCount(0);
88
95
  await page.close();
@@ -90,6 +97,7 @@ test.describe('Table', () => {
90
97
 
91
98
  test('delete column', async ({ browser, browserName }) => {
92
99
  test.skip(browserName === 'webkit');
100
+ test.skip(browserName === 'firefox');
93
101
  const { page } = await setupPage(browser, { url: storyUrl });
94
102
  const table = new TableManager(page);
95
103
 
@@ -113,59 +121,39 @@ test.describe('Table', () => {
113
121
  await page.close();
114
122
  });
115
123
 
116
- // Rest of add column test remains the same as it's a more complex flow.
117
- test('add column', async ({ browser, browserName }) => {
124
+ test('add column without changing format', async ({ browser, browserName }) => {
118
125
  test.skip(browserName === 'webkit');
126
+ test.skip(browserName === 'firefox');
119
127
  const { page } = await setupPage(browser, { url: storyUrl });
120
128
  const table = new TableManager(page);
121
129
 
122
130
  await table.grid.ready();
123
- const newColumnLabel = 'TEST LABEL';
131
+ const newColumnLabel = 'DEFAULT FORMAT COL';
124
132
 
125
- await table.addColumn({ label: newColumnLabel, format: 'number' });
133
+ await table.addColumn({ label: newColumnLabel });
126
134
 
127
135
  await expect(page.getByRole('gridcell', { name: newColumnLabel })).toBeVisible();
128
136
  await page.close();
129
137
  });
130
138
 
131
- test('reference > reference / create new object', async ({ browser, browserName }) => {
139
+ test('add column with format', async ({ browser, browserName }) => {
132
140
  test.skip(browserName === 'webkit');
141
+ test.skip(browserName === 'firefox');
133
142
  const { page } = await setupPage(browser, { url: storyUrl });
134
143
  const table = new TableManager(page);
135
144
 
136
145
  await table.grid.ready();
146
+ const newColumnLabel = 'TEST LABEL';
137
147
 
138
- // Reference existing object.
139
- // Scroll over to the manager column.
140
- await table.grid.panByWheel(10000, 0);
141
-
142
- await table.grid.cell(4, 0, 'grid').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();
147
-
148
- // Assert that the value is shown in the cell.
149
- await expect(page.getByRole('gridcell', { name: 'Aut.' }).first()).toBeVisible();
150
-
151
- // Create new object.
152
- await table.grid.cell(4, 1, 'grid').click();
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 });
158
- await page.getByText('Create new object', { exact: false }).click();
159
- await page.getByTestId('save-button').click();
160
-
161
- // Scroll to the left.
162
- await page.getByRole('gridcell', { name: 'test' }).first().click();
148
+ await table.addColumn({ label: newColumnLabel, format: 'Number' });
163
149
 
150
+ await expect(page.getByRole('gridcell', { name: newColumnLabel })).toBeVisible();
164
151
  await page.close();
165
152
  });
166
153
 
167
- test('test toggles', async ({ browser, browserName }) => {
154
+ test.skip('test toggles', async ({ browser, browserName }) => {
168
155
  test.skip(browserName === 'webkit');
156
+ test.skip(browserName === 'firefox');
169
157
  const { page } = await setupPage(browser, { url: storyUrl });
170
158
  const table = new TableManager(page);
171
159
 
@@ -180,14 +168,45 @@ test.describe('Table', () => {
180
168
  // Assert the first two switch checkboxes are checked.
181
169
  await expect(page.getByTestId('table-switch').first()).toBeChecked();
182
170
  await expect(page.getByTestId('table-switch').nth(1)).toBeChecked();
183
- await expect(table.grid.cell(0, 0, 'grid')).toHaveText('Sapiente.');
171
+ await expect(table.grid.cell(0, 0, 'grid')).toHaveText('Aut.');
184
172
  await expect(table.grid.cell(0, 1, 'grid')).toHaveText('Beatae.');
185
173
 
186
174
  await page.close();
187
175
  });
188
176
 
189
- test('extant relations work as expected', async ({ browser, browserName }) => {
177
+ test('add row and edit cell', async ({ browser, browserName }) => {
190
178
  test.skip(browserName === 'webkit');
179
+ test.skip(browserName === 'firefox');
180
+ const { page } = await setupPage(browser, { url: storyUrl });
181
+ const table = new TableManager(page);
182
+
183
+ await table.grid.ready();
184
+
185
+ // Click the CTA row at the bottom to add a new row.
186
+ await table.grid.cell(0, 0, 'frozenRowsEnd').click();
187
+
188
+ // All schema fields are optional so the row is added directly to the grid (not as a draft).
189
+ const newCell = table.grid.cell(0, 10, 'grid');
190
+ await newCell.waitFor({ state: 'visible' });
191
+
192
+ // Focus should already be on the new row; engage edit mode.
193
+ await page.keyboard.press('Enter');
194
+ await page.getByTestId('grid.cell-editor').waitFor({ state: 'visible' });
195
+
196
+ // Type text and confirm.
197
+ const cellContent = 'New row content';
198
+ await page.keyboard.type(cellContent);
199
+ await page.keyboard.press('Enter');
200
+
201
+ // Assert the cell content is saved.
202
+ await expect(newCell).toHaveText(cellContent);
203
+ await page.close();
204
+ });
205
+
206
+ // TODO(wittjosiah): Remove? Conflicts with story play function which is run as a unit test anyways.
207
+ test.skip('extant relations work as expected', async ({ browser, browserName }) => {
208
+ test.skip(browserName === 'webkit');
209
+ test.skip(browserName === 'firefox');
191
210
  const { page } = await setupPage(browser, { url: relationsStoryUrl });
192
211
 
193
212
  // Wait for the page to load
@@ -198,18 +217,22 @@ test.describe('Table', () => {
198
217
  const dxGrid = page.locator('dx-grid').nth(1);
199
218
  await dxGrid.waitFor({ state: 'visible' });
200
219
 
201
- // Scroll to the last column (column 8)
220
+ // Scroll to the last column (column 4)
202
221
  await dxGrid.evaluate(async (dxGridElement: DxGrid) => {
203
- dxGridElement.scrollToColumn(8);
222
+ dxGridElement.scrollToColumn(4);
204
223
  });
205
224
 
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"]');
225
+ // Click on the cell at aria-rowindex=0 aria-colindex=4 to focus it
226
+ const targetCell = dxGrid.locator('[data-dx-grid-plane="grid"] [aria-rowindex="0"][aria-colindex="4"]');
208
227
  await targetCell.click();
209
228
 
210
229
  // Click again to engage edit mode
211
230
  await page.keyboard.press('Enter');
212
- await page.getByTestId('grid.cell-editor').waitFor({ state: 'visible' });
231
+
232
+ // Click on the combobox to open options
233
+ await page.getByRole('combobox').click();
234
+ await page.pause();
235
+ await page.getByPlaceholder('Search…').focus();
213
236
 
214
237
  // Type the first few letters of an org name.
215
238
  const orgName =
@@ -226,14 +249,19 @@ test.describe('Table', () => {
226
249
  // Type the enter key
227
250
  await page.keyboard.press('Enter');
228
251
 
252
+ // Save the result
253
+ await page.getByTestId('save-button').click();
254
+
229
255
  // Assert that the cell element has the org name
230
256
  await expect(targetCell).toHaveText(orgName);
231
257
 
232
258
  await page.close();
233
259
  });
234
260
 
235
- test('new relations work as expected', async ({ browser, browserName }) => {
261
+ // TODO(wittjosiah): Remove? Conflicts with story play function which is run as a unit test anyways.
262
+ test.skip('new relations work as expected', async ({ browser, browserName }) => {
236
263
  test.skip(browserName === 'webkit');
264
+ test.skip(browserName === 'firefox');
237
265
  const { page } = await setupPage(browser, { url: relationsStoryUrl });
238
266
 
239
267
  // Wait for the page to load
@@ -244,18 +272,21 @@ test.describe('Table', () => {
244
272
  const dxGrid = page.locator('dx-grid').nth(1);
245
273
  await dxGrid.waitFor({ state: 'visible' });
246
274
 
247
- // Scroll to the last column (column 8)
275
+ // Scroll to the last column (column 4)
248
276
  await dxGrid.evaluate(async (dxGridElement: DxGrid) => {
249
- dxGridElement.scrollToColumn(8);
277
+ dxGridElement.scrollToColumn(4);
250
278
  });
251
279
 
252
280
  // 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"]');
281
+ const targetCell = dxGrid.locator('[data-dx-grid-plane="grid"] [aria-rowindex="0"][aria-colindex="4"]');
254
282
  await targetCell.click();
255
283
 
256
284
  // Click again to engage edit mode
257
285
  await page.keyboard.press('Enter');
258
- await page.getByTestId('grid.cell-editor').waitFor({ state: 'visible' });
286
+
287
+ // Click on the combobox to open options
288
+ await page.getByRole('combobox').click();
289
+ await page.getByPlaceholder('Search…').focus();
259
290
 
260
291
  // Type the first few letters of an org name.
261
292
  const orgName = 'Sally';
@@ -266,13 +297,28 @@ test.describe('Table', () => {
266
297
 
267
298
  // Type the enter key
268
299
  await page.keyboard.press('Enter');
300
+ await page.pause();
269
301
 
270
302
  // Click the save button in the popover
271
- await page.getByTestId('save-button').click();
303
+ await page.getByTestId('create-referenced-object-form').getByTestId('save-button').click();
272
304
 
273
305
  // Assert that the cell element has the org name
274
306
  await expect(targetCell).toHaveText(orgName);
275
307
 
308
+ // Make a change to a non-ref cell to check that populated refs don’t cause problems with snapshots or schemas.
309
+ await dxGrid.evaluate(async (dxGridElement: DxGrid) => {
310
+ dxGridElement.scrollToColumn(6);
311
+ });
312
+
313
+ const nonRefContent = random.lorem.words(3);
314
+ const nonRefCell = dxGrid.locator('[data-dx-grid-plane="grid"] [aria-rowindex="0"][aria-colindex="6"]');
315
+ await nonRefCell.click();
316
+ await page.keyboard.press('Enter');
317
+ await page.getByTestId('grid.cell-editor').waitFor({ state: 'visible' });
318
+ await page.keyboard.type(nonRefContent, { delay: 500 });
319
+ await page.keyboard.press('Enter');
320
+ await expect(nonRefCell).toHaveText(nonRefContent);
321
+
276
322
  await page.close();
277
323
  });
278
324
  });
@@ -2,40 +2,52 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { useCallback, useMemo, useRef } from 'react';
5
+ import { RegistryContext } from '@effect-atom/atom-react';
6
+ import { type RefObject, useCallback, useContext, useMemo, useRef } from 'react';
6
7
 
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';
8
+ import { type Database, type Type } from '@dxos/echo';
9
+ import { isMutable } from '@dxos/echo/internal';
10
+ import { random } from '@dxos/random';
11
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';
12
+ import { useClientStory } from '@dxos/react-client/testing';
13
+ import { useGlobalFilteredObjects } from '@dxos/react-ui-search';
14
+ import { type ProjectionModel, getTypenameFromQuery } from '@dxos/schema';
14
15
 
15
16
  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.
17
+ import { useAddRow, useProjectionModel, useTableModel } from '../hooks';
18
+ import { type TableModel, TablePresentation } from '../model';
19
+ import { Table } from '../types';
20
+
21
+ random.seed(0); // NOTE(ZaymonFC): Required for smoke tests.
22
+
23
+ export type TestTableModel<T extends Type.AnyObj = Type.AnyObj> = {
24
+ schema: T | undefined;
25
+ table: Table.Table | undefined;
26
+ projection: ProjectionModel | undefined;
27
+ tableRef: RefObject<TableController | null>;
28
+ model: TableModel | undefined;
29
+ presentation: TablePresentation | undefined;
30
+ db: Database.Database | undefined;
31
+ handleInsertRow: () => void;
32
+ handleSaveView: () => void;
33
+ handleDeleteRows: (rowIndex: number, objects: any[]) => void;
34
+ handleDeleteColumn: (fieldId: string) => void;
35
+ };
20
36
 
21
37
  /**
22
38
  * Custom hook to create and manage a test table model for storybook demonstrations.
23
39
  * Provides table data, schema, and handlers for table operations.
24
40
  */
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]);
41
+ export const useTestTableModel = <T extends Type.AnyObj = Type.AnyObj>(): TestTableModel<T> => {
42
+ const registry = useContext(RegistryContext);
43
+ const { space } = useClientStory();
44
+ const db = space?.db;
45
+
46
+ const tables = useQuery(space?.db, Filter.type(Table.Table));
47
+ const table = tables.at(0);
48
+ const typename = table?.view.target?.query ? getTypenameFromQuery(table.view.target.query.ast) : undefined;
49
+ const schema = useSchema<T>(space?.db, typename);
50
+ const projection = useProjectionModel(schema, table, registry);
39
51
 
40
52
  const features = useMemo(
41
53
  () => ({
@@ -46,7 +58,7 @@ export const useTestTableModel = () => {
46
58
  [schema],
47
59
  );
48
60
 
49
- const objects = useQuery(space, schema ? Filter.type(schema) : Filter.nothing());
61
+ const objects = useQuery(db, schema ? Filter.type(schema) : Filter.nothing());
50
62
  const filteredObjects = useGlobalFilteredObjects(objects);
51
63
 
52
64
  const tableRef = useRef<TableController>(null);
@@ -58,15 +70,15 @@ export const useTestTableModel = () => {
58
70
  tableRef.current?.update?.();
59
71
  }, []);
60
72
 
61
- const addRow = useAddRow({ space, schema });
73
+ const addRow = useAddRow({ db, schema });
62
74
 
63
75
  const handleDeleteRows = useCallback(
64
76
  (_: number, objects: any[]) => {
65
77
  for (const object of objects) {
66
- space?.db.remove(object);
78
+ db?.remove(object);
67
79
  }
68
80
  },
69
- [space],
81
+ [db],
70
82
  );
71
83
 
72
84
  const handleDeleteColumn = useCallback(
@@ -79,22 +91,21 @@ export const useTestTableModel = () => {
79
91
  );
80
92
 
81
93
  const model = useTableModel({
82
- view,
83
- schema: jsonSchema,
94
+ object: table,
84
95
  projection,
85
96
  features,
86
97
  rows: filteredObjects,
87
98
  onInsertRow: addRow,
88
99
  onDeleteRows: handleDeleteRows,
89
- onDeleteColumn: handleDeleteColumn,
100
+ onColumnDelete: handleDeleteColumn,
90
101
  onCellUpdate: handleCellUpdate,
91
102
  onRowOrderChange: handleRowOrderChange,
92
103
  });
93
104
 
94
105
  const handleInsertRow = useCallback(() => {
95
- model?.insertRow();
96
- tableRef.current?.focusDraft?.();
97
- }, [model, tableRef.current]);
106
+ const insertResult = model?.insertRow();
107
+ tableRef.current?.handleInsertRowResult?.(insertResult);
108
+ }, [model]);
98
109
 
99
110
  const handleSaveView = useCallback(() => {
100
111
  model?.saveView();
@@ -102,19 +113,18 @@ export const useTestTableModel = () => {
102
113
 
103
114
  const presentation = useMemo(() => {
104
115
  if (model) {
105
- return new TablePresentation(model);
116
+ return new TablePresentation(registry, model);
106
117
  }
107
- }, [model]);
118
+ }, [registry, model]);
108
119
 
109
120
  return {
110
121
  schema,
111
- view,
122
+ table,
112
123
  projection,
113
124
  tableRef,
114
125
  model,
115
126
  presentation,
116
- space,
117
- client,
127
+ db,
118
128
  handleInsertRow,
119
129
  handleSaveView,
120
130
  handleDeleteRows,
@@ -4,33 +4,31 @@
4
4
 
5
5
  import { type Resource } from '@dxos/react-ui';
6
6
 
7
- export const translationKey = 'react-ui-table';
7
+ export const translationKey = '@dxos/react-ui-table';
8
8
 
9
9
  export const translations = [
10
10
  {
11
11
  'en-US': {
12
12
  [translationKey]: {
13
- 'object placeholder': 'New table',
14
- 'create object label': 'Create table',
15
- 'table name placeholder': 'Table name',
16
- 'settings title': 'Table settings',
17
- 'table schema label': 'Record type',
18
- 'new schema': 'New record type',
19
- 'continue label': 'Continue',
20
- 'create stack section label': 'Create table',
21
- 'add row label': 'Add row',
22
- 'create comment': 'Create thread',
23
- 'column action sort descending': 'Sort Descending',
24
- 'column action sort ascending': 'Sort Ascending',
25
- 'column action clear sorting': 'Clear Sorting',
26
- 'column action settings': 'Column Settings',
27
- 'column action delete': 'Delete Column',
28
- 'delete row label': 'Delete row',
29
- 'bulk delete row label': 'Delete selected rows',
30
- 'column deleted label': 'Column deleted',
31
- 'new column button label': 'Create column',
32
- 'create new object label': 'Create new object “{{text}}”',
33
- 'save view label': 'Save changes to view',
13
+ 'object.placeholder': 'New table',
14
+ 'create-object.label': 'Create table',
15
+ 'table-name.placeholder': 'Table name',
16
+ 'settings.title': 'Table settings',
17
+ 'table-schema.label': 'Type',
18
+ 'continue.label': 'Continue',
19
+ 'create-stack-section.label': 'Create table',
20
+ 'add-row.label': 'Add row',
21
+ 'column-action-sort-descending.menu': 'Sort Descending',
22
+ 'column-action-sort-ascending.menu': 'Sort Ascending',
23
+ 'column-action-clear-sorting.menu': 'Clear Sorting',
24
+ 'column-action-settings.menu': 'Column Settings',
25
+ 'column-action-delete.menu': 'Delete Column',
26
+ 'delete-row.label': 'Delete row',
27
+ 'bulk-delete-row.label': 'Delete selected rows',
28
+ 'column-deleted.label': 'Column deleted',
29
+ 'new-column-button.label': 'Create column',
30
+ 'create-new-object.label': 'Create new object “{{text}}”',
31
+ 'save-view.label': 'Save changes to view',
34
32
  },
35
33
  },
36
34
  },
@@ -0,0 +1,79 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Match from 'effect/Match';
6
+ import * as Schema from 'effect/Schema';
7
+ import * as SchemaAST from 'effect/SchemaAST';
8
+
9
+ import { Annotation, JsonSchema, Obj, Ref, Type } from '@dxos/echo';
10
+ import { View } from '@dxos/echo';
11
+ import { FormInputAnnotation, type JsonPath, type JsonSchemaType, LabelAnnotation } from '@dxos/echo/internal';
12
+ import { ViewAnnotation } from '@dxos/schema';
13
+
14
+ export const Table = Schema.Struct({
15
+ name: Schema.String.pipe(Schema.optional),
16
+
17
+ view: Ref.Ref(View.View).pipe(FormInputAnnotation.set(false)),
18
+
19
+ sizes: Schema.Record({
20
+ // TODO(wittjosiah): Should be JsonPath.
21
+ key: Schema.String,
22
+ value: Schema.Number,
23
+ }).pipe(Schema.mutable, FormInputAnnotation.set(false)),
24
+ }).pipe(
25
+ Type.object({
26
+ typename: 'org.dxos.type.table',
27
+ version: '0.1.0',
28
+ }),
29
+ LabelAnnotation.set(['name']),
30
+ ViewAnnotation.set(true),
31
+ Annotation.IconAnnotation.set({
32
+ icon: 'ph--table--regular',
33
+ hue: 'green',
34
+ }),
35
+ );
36
+
37
+ export interface Table extends Schema.Schema.Type<typeof Table> {}
38
+
39
+ type MakeProps = {
40
+ name?: string;
41
+ sizes?: Record<string, number>;
42
+ view: View.View;
43
+ /** Required to auto-size columns. */
44
+ jsonSchema?: JsonSchemaType;
45
+ };
46
+
47
+ /**
48
+ * Make a table as a view of a data set.
49
+ */
50
+ export const make = ({ name, sizes = {}, view, jsonSchema }: MakeProps): Table => {
51
+ const table = Obj.make(Table, { name, view: Ref.make(view), sizes } as Obj.MakeProps<typeof Table>);
52
+
53
+ // Preset sizes.
54
+ if (jsonSchema) {
55
+ const schema = JsonSchema.toEffectSchema(jsonSchema);
56
+ const properties = SchemaAST.getPropertySignatures(schema.ast);
57
+ for (const property of properties) {
58
+ const name = property.name.toString() as JsonPath;
59
+ if (sizes?.[name]) {
60
+ table.sizes[name] = sizes[name];
61
+ continue;
62
+ }
63
+
64
+ Match.type<SchemaAST.AST>().pipe(
65
+ Match.when({ _tag: 'BooleanKeyword' }, () => {
66
+ table.sizes[name] = 100;
67
+ }),
68
+ Match.when({ _tag: 'NumberKeyword' }, () => {
69
+ table.sizes[name] = 100;
70
+ }),
71
+ Match.orElse(() => {
72
+ // Noop.
73
+ }),
74
+ )(property.type);
75
+ }
76
+ }
77
+
78
+ return table;
79
+ };
@@ -2,4 +2,4 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- export * from './schema';
5
+ export * as Table from './Table';
@@ -0,0 +1,70 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { Registry } from '@effect-atom/atom-react';
6
+ import { describe, expect, test } from 'vitest';
7
+
8
+ import { Format } from '@dxos/echo';
9
+
10
+ import { type TablePropertyDefinition, getBaseSchema, makeDynamicTable } from './dynamic-table';
11
+
12
+ describe('makeDynamicTable', () => {
13
+ /**
14
+ * Base case: plain jsonSchema (not from Echo / JsonSchema.toJsonSchema). Does not exercise the path
15
+ * where projection or schema are reactive, so this does not reproduce the Obj.change regression.
16
+ * Confirms makeDynamicTable + setProperties (title) works with plain objects.
17
+ */
18
+ test('makeDynamicTable with plain jsonSchema and properties with title does not throw', () => {
19
+ const registry = Registry.make();
20
+ const jsonSchema = {
21
+ type: 'object' as const,
22
+ properties: {
23
+ id: { type: 'string' as const },
24
+ name: { type: 'string' as const },
25
+ },
26
+ required: ['id'],
27
+ };
28
+ const properties: TablePropertyDefinition[] = [
29
+ { name: 'id', format: Format.TypeFormat.String },
30
+ { name: 'name', format: Format.TypeFormat.String, title: 'Name' },
31
+ ];
32
+
33
+ expect(() => {
34
+ const { projection, object } = makeDynamicTable({
35
+ registry,
36
+ jsonSchema,
37
+ properties,
38
+ });
39
+ expect(projection).toBeDefined();
40
+ expect(object).toBeDefined();
41
+ expect(projection.getFields().length).toBeGreaterThan(0);
42
+ }).not.toThrow();
43
+ });
44
+
45
+ /**
46
+ * Regression test for the path where jsonSchema comes from getBaseSchema(typename, properties).
47
+ * That path uses Echo (ViewModel.make, JsonSchema.toJsonSchema); mutations must run inside Obj.change and on a
48
+ * cloned schema. This test ensures that flow does not throw.
49
+ */
50
+ test('makeDynamicTable with jsonSchema from getBaseSchema(typename, properties) and properties with title does not throw', () => {
51
+ const registry = Registry.make();
52
+ const typename = 'com.example.type.dynamic-table-test';
53
+ const properties: TablePropertyDefinition[] = [
54
+ { name: 'id', format: Format.TypeFormat.String },
55
+ { name: 'name', format: Format.TypeFormat.String, title: 'Name' },
56
+ ];
57
+ const { jsonSchema } = getBaseSchema({ typename, properties });
58
+
59
+ expect(() => {
60
+ const { projection, object } = makeDynamicTable({
61
+ registry,
62
+ jsonSchema,
63
+ properties,
64
+ });
65
+ expect(projection).toBeDefined();
66
+ expect(object).toBeDefined();
67
+ expect(projection.getFields().length).toBeGreaterThan(0);
68
+ }).not.toThrow();
69
+ });
70
+ });