@dxos/react-ui-table 0.8.4-main.fd6878d → 0.9.0

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 (183) hide show
  1. package/LICENSE +102 -5
  2. package/dist/lib/browser/chunk-4KOQTPFC.mjs +1702 -0
  3. package/dist/lib/browser/chunk-4KOQTPFC.mjs.map +7 -0
  4. package/dist/lib/browser/chunk-62PYOJPY.mjs +67 -0
  5. package/dist/lib/browser/chunk-62PYOJPY.mjs.map +7 -0
  6. package/dist/lib/browser/chunk-J5LGTIGS.mjs +10 -0
  7. package/dist/lib/browser/chunk-J5LGTIGS.mjs.map +7 -0
  8. package/dist/lib/browser/index.mjs +1151 -1175
  9. package/dist/lib/browser/index.mjs.map +4 -4
  10. package/dist/lib/browser/meta.json +1 -1
  11. package/dist/lib/browser/testing/index.mjs +37 -45
  12. package/dist/lib/browser/testing/index.mjs.map +3 -3
  13. package/dist/lib/browser/translations.mjs +36 -0
  14. package/dist/lib/browser/translations.mjs.map +7 -0
  15. package/dist/lib/browser/types/index.mjs +4 -3
  16. package/dist/lib/node-esm/chunk-7LMOZUTS.mjs +68 -0
  17. package/dist/lib/node-esm/chunk-7LMOZUTS.mjs.map +7 -0
  18. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +11 -0
  19. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +7 -0
  20. package/dist/lib/node-esm/chunk-KOSV2J5N.mjs +1704 -0
  21. package/dist/lib/node-esm/chunk-KOSV2J5N.mjs.map +7 -0
  22. package/dist/lib/node-esm/index.mjs +1151 -1175
  23. package/dist/lib/node-esm/index.mjs.map +4 -4
  24. package/dist/lib/node-esm/meta.json +1 -1
  25. package/dist/lib/node-esm/testing/index.mjs +37 -45
  26. package/dist/lib/node-esm/testing/index.mjs.map +3 -3
  27. package/dist/lib/node-esm/translations.mjs +37 -0
  28. package/dist/lib/node-esm/translations.mjs.map +7 -0
  29. package/dist/lib/node-esm/types/index.mjs +4 -3
  30. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts +1 -2
  31. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts.map +1 -1
  32. package/dist/types/src/components/Table/ColumnSettings.d.ts +2 -2
  33. package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
  34. package/dist/types/src/components/Table/DynamicTable.d.ts +6 -7
  35. package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
  36. package/dist/types/src/components/Table/DynamicTable.stories.d.ts +36 -3
  37. package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
  38. package/dist/types/src/components/Table/Relations.stories.d.ts +112 -5
  39. package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -1
  40. package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
  41. package/dist/types/src/components/Table/Table.d.ts +21 -19
  42. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  43. package/dist/types/src/components/Table/Table.stories.d.ts +115 -5
  44. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  45. package/dist/types/src/components/Table/TableContent.d.ts +18 -0
  46. package/dist/types/src/components/Table/TableContent.d.ts.map +1 -0
  47. package/dist/types/src/components/Table/TableToolbar.d.ts +21 -0
  48. package/dist/types/src/components/Table/TableToolbar.d.ts.map +1 -0
  49. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts +1 -1
  50. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts.map +1 -1
  51. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +12 -9
  52. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
  53. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +7 -14
  54. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  55. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +38 -5
  56. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  57. package/dist/types/src/components/TableCellEditor/index.d.ts +1 -0
  58. package/dist/types/src/components/TableCellEditor/index.d.ts.map +1 -1
  59. package/dist/types/src/components/index.d.ts +0 -1
  60. package/dist/types/src/components/index.d.ts.map +1 -1
  61. package/dist/types/src/components/testing.d.ts +23 -16
  62. package/dist/types/src/components/testing.d.ts.map +1 -1
  63. package/dist/types/src/hooks/index.d.ts +2 -1
  64. package/dist/types/src/hooks/index.d.ts.map +1 -1
  65. package/dist/types/src/hooks/useAddRow.d.ts +6 -6
  66. package/dist/types/src/hooks/useAddRow.d.ts.map +1 -1
  67. package/dist/types/src/hooks/useProjectionModel.d.ts +6 -0
  68. package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -0
  69. package/dist/types/src/hooks/useTableModel.d.ts +9 -9
  70. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  71. package/dist/types/src/index.d.ts +0 -2
  72. package/dist/types/src/index.d.ts.map +1 -1
  73. package/dist/types/src/model/modal-controller.d.ts +5 -8
  74. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  75. package/dist/types/src/model/selection-model.d.ts +18 -9
  76. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  77. package/dist/types/src/model/table-model.d.ts +111 -30
  78. package/dist/types/src/model/table-model.d.ts.map +1 -1
  79. package/dist/types/src/model/table-presentation.d.ts +4 -2
  80. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  81. package/dist/types/src/playwright/TableManager.d.ts +2 -1
  82. package/dist/types/src/playwright/TableManager.d.ts.map +1 -1
  83. package/dist/types/src/playwright/playwright.config.d.ts.map +1 -1
  84. package/dist/types/src/testing/useTestTableModel.d.ts +18 -34
  85. package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -1
  86. package/dist/types/src/translations.d.ts +21 -23
  87. package/dist/types/src/translations.d.ts.map +1 -1
  88. package/dist/types/src/types/Table.d.ts +32 -0
  89. package/dist/types/src/types/Table.d.ts.map +1 -0
  90. package/dist/types/src/types/index.d.ts +1 -1
  91. package/dist/types/src/types/index.d.ts.map +1 -1
  92. package/dist/types/src/util/dynamic-table.d.ts +14 -12
  93. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  94. package/dist/types/src/util/dynamic-table.test.d.ts +2 -0
  95. package/dist/types/src/util/dynamic-table.test.d.ts.map +1 -0
  96. package/dist/types/src/util/index.d.ts +2 -1
  97. package/dist/types/src/util/index.d.ts.map +1 -1
  98. package/dist/types/src/util/query-ast.d.ts +7 -0
  99. package/dist/types/src/util/query-ast.d.ts.map +1 -0
  100. package/dist/types/src/util/schema.d.ts +1 -1
  101. package/dist/types/src/util/schema.d.ts.map +1 -1
  102. package/dist/types/src/util/signals.d.ts.map +1 -1
  103. package/dist/types/src/util/sort.d.ts +12 -0
  104. package/dist/types/src/util/sort.d.ts.map +1 -0
  105. package/dist/types/src/util/table-buttons.d.ts +20 -20
  106. package/dist/types/src/util/table-buttons.d.ts.map +1 -1
  107. package/dist/types/src/util/table-controls.d.ts +4 -4
  108. package/dist/types/src/util/table-controls.d.ts.map +1 -1
  109. package/dist/types/src/util/tag.d.ts.map +1 -1
  110. package/dist/types/tsconfig.tsbuildinfo +1 -1
  111. package/package.json +68 -77
  112. package/src/components/Table/ColumnActionsMenu.tsx +17 -12
  113. package/src/components/Table/ColumnSettings.tsx +8 -9
  114. package/src/components/Table/DynamicTable.stories.tsx +41 -38
  115. package/src/components/Table/DynamicTable.tsx +32 -36
  116. package/src/components/Table/Relations.stories.tsx +219 -61
  117. package/src/components/Table/RowActionsMenu.tsx +27 -17
  118. package/src/components/Table/Table.stories.tsx +217 -141
  119. package/src/components/Table/Table.tsx +46 -391
  120. package/src/components/Table/TableContent.tsx +426 -0
  121. package/src/components/Table/TableToolbar.tsx +111 -0
  122. package/src/components/TableCellEditor/CellValidationMessage.tsx +5 -6
  123. package/src/components/TableCellEditor/FormCellEditor.tsx +136 -41
  124. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +39 -73
  125. package/src/components/TableCellEditor/TableCellEditor.tsx +81 -223
  126. package/src/components/TableCellEditor/index.ts +1 -0
  127. package/src/components/index.ts +0 -1
  128. package/src/components/testing.ts +42 -21
  129. package/src/hooks/index.ts +2 -1
  130. package/src/hooks/useAddRow.ts +14 -14
  131. package/src/hooks/useProjectionModel.ts +38 -0
  132. package/src/hooks/useTableModel.ts +36 -34
  133. package/src/index.ts +0 -4
  134. package/src/model/modal-controller.ts +23 -40
  135. package/src/model/selection-model.ts +75 -44
  136. package/src/model/table-model.test.ts +86 -56
  137. package/src/model/table-model.ts +464 -182
  138. package/src/model/table-presentation.test.ts +64 -59
  139. package/src/model/table-presentation.ts +107 -83
  140. package/src/playwright/TableManager.ts +13 -6
  141. package/src/playwright/playwright.config.ts +1 -1
  142. package/src/playwright/smoke.spec.ts +92 -46
  143. package/src/testing/useTestTableModel.ts +51 -42
  144. package/src/translations.ts +20 -22
  145. package/src/types/Table.ts +80 -0
  146. package/src/types/index.ts +1 -1
  147. package/src/util/dynamic-table.test.ts +63 -0
  148. package/src/util/dynamic-table.ts +53 -39
  149. package/src/util/index.ts +2 -1
  150. package/src/util/query-ast.ts +22 -0
  151. package/src/util/schema.ts +4 -4
  152. package/src/util/sort.ts +90 -0
  153. package/src/util/table-buttons.ts +3 -3
  154. package/src/util/table-controls.ts +2 -2
  155. package/dist/lib/browser/chunk-L2LSB6KV.mjs +0 -18
  156. package/dist/lib/browser/chunk-L2LSB6KV.mjs.map +0 -7
  157. package/dist/lib/browser/chunk-ZAOSIG3L.mjs +0 -1814
  158. package/dist/lib/browser/chunk-ZAOSIG3L.mjs.map +0 -7
  159. package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs +0 -20
  160. package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs.map +0 -7
  161. package/dist/lib/node-esm/chunk-ZOFFI3JJ.mjs +0 -1815
  162. package/dist/lib/node-esm/chunk-ZOFFI3JJ.mjs.map +0 -7
  163. package/dist/types/src/components/Table/CreateRefPanel.d.ts +0 -9
  164. package/dist/types/src/components/Table/CreateRefPanel.d.ts.map +0 -1
  165. package/dist/types/src/components/TableCellEditor/extension.d.ts +0 -13
  166. package/dist/types/src/components/TableCellEditor/extension.d.ts.map +0 -1
  167. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +0 -21
  168. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +0 -1
  169. package/dist/types/src/components/TableToolbar/index.d.ts +0 -2
  170. package/dist/types/src/components/TableToolbar/index.d.ts.map +0 -1
  171. package/dist/types/src/model/table-sorting.d.ts +0 -53
  172. package/dist/types/src/model/table-sorting.d.ts.map +0 -1
  173. package/dist/types/src/types/schema.d.ts +0 -8
  174. package/dist/types/src/types/schema.d.ts.map +0 -1
  175. package/dist/types/src/util/table.d.ts +0 -12
  176. package/dist/types/src/util/table.d.ts.map +0 -1
  177. package/src/components/Table/CreateRefPanel.tsx +0 -86
  178. package/src/components/TableCellEditor/extension.ts +0 -61
  179. package/src/components/TableToolbar/TableToolbar.tsx +0 -102
  180. package/src/components/TableToolbar/index.ts +0 -5
  181. package/src/model/table-sorting.ts +0 -216
  182. package/src/types/schema.ts +0 -16
  183. 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,51 +2,62 @@
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';
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';
8
+ import { type Database, Filter, Type } from '@dxos/echo';
9
+ import { random } from '@dxos/random';
10
+ import { useQuery, useType } from '@dxos/react-client/echo';
11
+ import { useClientStory } from '@dxos/react-client/testing';
12
+ import { useGlobalFilteredObjects } from '@dxos/react-ui-search';
13
+ import { type ProjectionModel, getTypeURIFromQuery } from '@dxos/schema';
14
14
 
15
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.
16
+ import { useAddRow, useProjectionModel, useTableModel } from '../hooks';
17
+ import { type TableModel, TablePresentation } from '../model';
18
+ import { Table } from '../types';
19
+
20
+ random.seed(0); // NOTE(ZaymonFC): Required for smoke tests.
21
+
22
+ export type TestTableModel<T extends Type.AnyObj = Type.AnyObj> = {
23
+ schema: T | undefined;
24
+ table: Table.Table | undefined;
25
+ projection: ProjectionModel | undefined;
26
+ tableRef: RefObject<TableController | null>;
27
+ model: TableModel | undefined;
28
+ presentation: TablePresentation | undefined;
29
+ db: Database.Database | undefined;
30
+ handleInsertRow: () => void;
31
+ handleSaveView: () => void;
32
+ handleDeleteRows: (rowIndex: number, objects: any[]) => void;
33
+ handleDeleteColumn: (fieldId: string) => void;
34
+ };
20
35
 
21
36
  /**
22
37
  * Custom hook to create and manage a test table model for storybook demonstrations.
23
38
  * Provides table data, schema, and handlers for table operations.
24
39
  */
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]);
40
+ export const useTestTableModel = <T extends Type.AnyObj = Type.AnyObj>(): TestTableModel<T> => {
41
+ const registry = useContext(RegistryContext);
42
+ const { space } = useClientStory();
43
+ const db = space?.db;
44
+
45
+ const tables = useQuery(space?.db, Filter.type(Table.Table));
46
+ const table = tables.at(0);
47
+ const typeUri = table?.view.target?.query ? getTypeURIFromQuery(table.view.target.query.ast) : undefined;
48
+ const schema = useType<T>(space?.db, typeUri);
49
+ const projection = useProjectionModel(schema, table, registry);
39
50
 
40
51
  const features = useMemo(
41
52
  () => ({
42
53
  selection: { enabled: true, mode: 'multiple' as const },
43
54
  dataEditable: true,
44
- schemaEditable: schema && isMutable(schema),
55
+ schemaEditable: schema != null && Type.getDatabase(schema) != null,
45
56
  }),
46
57
  [schema],
47
58
  );
48
59
 
49
- const objects = useQuery(space, schema ? Filter.type(schema) : Filter.nothing());
60
+ const objects = useQuery(db, schema ? Filter.type(schema) : Filter.nothing());
50
61
  const filteredObjects = useGlobalFilteredObjects(objects);
51
62
 
52
63
  const tableRef = useRef<TableController>(null);
@@ -58,15 +69,15 @@ export const useTestTableModel = () => {
58
69
  tableRef.current?.update?.();
59
70
  }, []);
60
71
 
61
- const addRow = useAddRow({ space, schema });
72
+ const addRow = useAddRow({ db, schema });
62
73
 
63
74
  const handleDeleteRows = useCallback(
64
75
  (_: number, objects: any[]) => {
65
76
  for (const object of objects) {
66
- space?.db.remove(object);
77
+ db?.remove(object);
67
78
  }
68
79
  },
69
- [space],
80
+ [db],
70
81
  );
71
82
 
72
83
  const handleDeleteColumn = useCallback(
@@ -79,22 +90,21 @@ export const useTestTableModel = () => {
79
90
  );
80
91
 
81
92
  const model = useTableModel({
82
- view,
83
- schema: jsonSchema,
93
+ object: table,
84
94
  projection,
85
95
  features,
86
96
  rows: filteredObjects,
87
97
  onInsertRow: addRow,
88
98
  onDeleteRows: handleDeleteRows,
89
- onDeleteColumn: handleDeleteColumn,
99
+ onColumnDelete: handleDeleteColumn,
90
100
  onCellUpdate: handleCellUpdate,
91
101
  onRowOrderChange: handleRowOrderChange,
92
102
  });
93
103
 
94
104
  const handleInsertRow = useCallback(() => {
95
- model?.insertRow();
96
- tableRef.current?.focusDraft?.();
97
- }, [model, tableRef.current]);
105
+ const insertResult = model?.insertRow();
106
+ tableRef.current?.handleInsertRowResult?.(insertResult);
107
+ }, [model]);
98
108
 
99
109
  const handleSaveView = useCallback(() => {
100
110
  model?.saveView();
@@ -102,19 +112,18 @@ export const useTestTableModel = () => {
102
112
 
103
113
  const presentation = useMemo(() => {
104
114
  if (model) {
105
- return new TablePresentation(model);
115
+ return new TablePresentation(registry, model);
106
116
  }
107
- }, [model]);
117
+ }, [registry, model]);
108
118
 
109
119
  return {
110
120
  schema,
111
- view,
121
+ table,
112
122
  projection,
113
123
  tableRef,
114
124
  model,
115
125
  presentation,
116
- space,
117
- client,
126
+ db,
118
127
  handleInsertRow,
119
128
  handleSaveView,
120
129
  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,80 @@
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
+ // QueryAST is referenced indirectly through `Type.InstanceType<typeof TableSchema>`
10
+ // (Ref.Ref(View.View) → View.View → QueryAST.Query) in the emitted .d.ts; the
11
+ // namespace import keeps the inferred types portable.
12
+ // eslint-disable-next-line unused-imports/no-unused-imports
13
+ import { DXN, Annotation, JsonSchema, Obj, QueryAST, Ref, Type, View } from '@dxos/echo';
14
+ import { FormInputAnnotation, LabelAnnotation } from '@dxos/echo/Annotation';
15
+ import { type JsonSchema as JsonSchemaType } from '@dxos/echo/JsonSchema';
16
+ import { SchemaEx } from '@dxos/effect';
17
+ import { ViewAnnotation } from '@dxos/schema';
18
+
19
+ const TableSchema = Schema.Struct({
20
+ name: Schema.String.pipe(Schema.optional),
21
+
22
+ view: Ref.Ref(View.View).pipe(FormInputAnnotation.set(false)),
23
+
24
+ sizes: Schema.Record({
25
+ // TODO(wittjosiah): Should be JsonPath.
26
+ key: Schema.String,
27
+ value: Schema.Number,
28
+ }).pipe(Schema.mutable, FormInputAnnotation.set(false)),
29
+ }).pipe(
30
+ LabelAnnotation.set(['name']),
31
+ ViewAnnotation.set(['view']),
32
+ Annotation.IconAnnotation.set({ icon: 'ph--table--regular', hue: 'green' }),
33
+ Type.makeObject(DXN.make('org.dxos.type.table', '0.1.0')),
34
+ );
35
+
36
+ // TODO(wittjosiah): Try to clean up this type inference.
37
+ export interface Table extends Type.InstanceType<typeof TableSchema> {}
38
+ export const Table = TableSchema;
39
+
40
+ type MakeProps = {
41
+ name?: string;
42
+ sizes?: Record<string, number>;
43
+ view: View.View;
44
+ /** Required to auto-size columns. */
45
+ jsonSchema?: JsonSchemaType;
46
+ };
47
+
48
+ /**
49
+ * Make a table as a view of a data set.
50
+ */
51
+ export const make = ({ name, sizes = {}, view, jsonSchema }: MakeProps): Table => {
52
+ const table = Obj.make(Table, { name, view: Ref.make(view), sizes } as Obj.MakeProps<typeof Table>);
53
+
54
+ // Preset sizes.
55
+ if (jsonSchema) {
56
+ const schema = JsonSchema.toEffectSchema(jsonSchema);
57
+ const properties = SchemaAST.getPropertySignatures(schema.ast);
58
+ for (const property of properties) {
59
+ const name = property.name.toString() as SchemaEx.JsonPath;
60
+ if (sizes?.[name]) {
61
+ table.sizes[name] = sizes[name];
62
+ continue;
63
+ }
64
+
65
+ Match.type<SchemaAST.AST>().pipe(
66
+ Match.when({ _tag: 'BooleanKeyword' }, () => {
67
+ table.sizes[name] = 100;
68
+ }),
69
+ Match.when({ _tag: 'NumberKeyword' }, () => {
70
+ table.sizes[name] = 100;
71
+ }),
72
+ Match.orElse(() => {
73
+ // Noop.
74
+ }),
75
+ )(property.type);
76
+ }
77
+ }
78
+
79
+ return table;
80
+ };
@@ -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,63 @@
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
+ * Builds the Type entity from a JSON schema, then makes the table.
15
+ * Confirms makeDynamicTable + setProperties (title) works for the jsonSchema path.
16
+ */
17
+ test('makeDynamicTable from a jsonSchema with properties with title does not throw', () => {
18
+ const registry = Registry.make();
19
+ const jsonSchema = {
20
+ typename: 'com.example.type.dynamicTablePlain',
21
+ version: '0.1.0',
22
+ type: 'object' as const,
23
+ properties: {
24
+ id: { type: 'string' as const },
25
+ name: { type: 'string' as const },
26
+ },
27
+ required: ['id'],
28
+ };
29
+ const properties: TablePropertyDefinition[] = [
30
+ { name: 'id', format: Format.TypeFormat.String },
31
+ { name: 'name', format: Format.TypeFormat.String, title: 'Name' },
32
+ ];
33
+
34
+ expect(() => {
35
+ const { type } = getBaseSchema({ jsonSchema });
36
+ const { projection, object } = makeDynamicTable({ registry, type, properties });
37
+ expect(projection).toBeDefined();
38
+ expect(object).toBeDefined();
39
+ expect(projection.getFields().length).toBeGreaterThan(0);
40
+ }).not.toThrow();
41
+ });
42
+
43
+ /**
44
+ * Builds the Type entity from property definitions, then makes the table.
45
+ * Confirms makeDynamicTable + setProperties (title) works for the property-definitions path.
46
+ */
47
+ test('makeDynamicTable from getBaseSchema(typename, properties) with properties with title does not throw', () => {
48
+ const registry = Registry.make();
49
+ const typename = 'com.example.type.dynamicTableTest';
50
+ const properties: TablePropertyDefinition[] = [
51
+ { name: 'id', format: Format.TypeFormat.String },
52
+ { name: 'name', format: Format.TypeFormat.String, title: 'Name' },
53
+ ];
54
+
55
+ expect(() => {
56
+ const { type } = getBaseSchema({ typename, properties });
57
+ const { projection, object } = makeDynamicTable({ registry, type, properties });
58
+ expect(projection).toBeDefined();
59
+ expect(object).toBeDefined();
60
+ expect(projection.getFields().length).toBeGreaterThan(0);
61
+ }).not.toThrow();
62
+ });
63
+ });