@dxos/react-ui-table 0.8.4-main.67995b8 → 0.8.4-main.69d29f4

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 (155) hide show
  1. package/dist/lib/browser/chunk-I5IFW7UF.mjs +1746 -0
  2. package/dist/lib/browser/chunk-I5IFW7UF.mjs.map +7 -0
  3. package/dist/lib/browser/chunk-OZBLY7XM.mjs +76 -0
  4. package/dist/lib/browser/chunk-OZBLY7XM.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +1163 -1120
  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 +30 -37
  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-DDJKYJFY.mjs +1748 -0
  12. package/dist/lib/node-esm/chunk-DDJKYJFY.mjs.map +7 -0
  13. package/dist/lib/node-esm/chunk-WNZSKIRF.mjs +77 -0
  14. package/dist/lib/node-esm/chunk-WNZSKIRF.mjs.map +7 -0
  15. package/dist/lib/node-esm/index.mjs +1163 -1120
  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 +30 -37
  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 +2 -3
  22. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts.map +1 -1
  23. package/dist/types/src/components/Table/ColumnSettings.d.ts +3 -3
  24. package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
  25. package/dist/types/src/components/Table/DynamicTable.d.ts +5 -6
  26. package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
  27. package/dist/types/src/components/Table/DynamicTable.stories.d.ts +37 -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 +111 -0
  30. package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -0
  31. package/dist/types/src/components/Table/RowActionsMenu.d.ts +1 -1
  32. package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
  33. package/dist/types/src/components/Table/Table.d.ts +13 -9
  34. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  35. package/dist/types/src/components/Table/Table.stories.d.ts +108 -4
  36. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  37. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts +1 -1
  38. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts.map +1 -1
  39. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +13 -9
  40. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
  41. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +11 -18
  42. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  43. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +39 -4
  44. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  45. package/dist/types/src/components/TableCellEditor/index.d.ts +1 -0
  46. package/dist/types/src/components/TableCellEditor/index.d.ts.map +1 -1
  47. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +4 -4
  48. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -1
  49. package/dist/types/src/components/testing.d.ts +23 -16
  50. package/dist/types/src/components/testing.d.ts.map +1 -1
  51. package/dist/types/src/hooks/index.d.ts +2 -1
  52. package/dist/types/src/hooks/index.d.ts.map +1 -1
  53. package/dist/types/src/hooks/useAddRow.d.ts +6 -6
  54. package/dist/types/src/hooks/useAddRow.d.ts.map +1 -1
  55. package/dist/types/src/hooks/useProjectionModel.d.ts +6 -0
  56. package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -0
  57. package/dist/types/src/hooks/useTableModel.d.ts +9 -9
  58. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  59. package/dist/types/src/model/modal-controller.d.ts +5 -8
  60. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  61. package/dist/types/src/model/selection-model.d.ts +17 -8
  62. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  63. package/dist/types/src/model/table-model.d.ts +109 -28
  64. package/dist/types/src/model/table-model.d.ts.map +1 -1
  65. package/dist/types/src/model/table-presentation.d.ts +5 -2
  66. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  67. package/dist/types/src/playwright/TableManager.d.ts +1 -1
  68. package/dist/types/src/playwright/TableManager.d.ts.map +1 -1
  69. package/dist/types/src/testing/useTestTableModel.d.ts +18 -34
  70. package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -1
  71. package/dist/types/src/translations.d.ts +4 -4
  72. package/dist/types/src/translations.d.ts.map +1 -1
  73. package/dist/types/src/types/Table.d.ts +31 -0
  74. package/dist/types/src/types/Table.d.ts.map +1 -0
  75. package/dist/types/src/types/index.d.ts +1 -1
  76. package/dist/types/src/types/index.d.ts.map +1 -1
  77. package/dist/types/src/util/dynamic-table.d.ts +13 -12
  78. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  79. package/dist/types/src/util/index.d.ts +2 -1
  80. package/dist/types/src/util/index.d.ts.map +1 -1
  81. package/dist/types/src/util/query-ast.d.ts +7 -0
  82. package/dist/types/src/util/query-ast.d.ts.map +1 -0
  83. package/dist/types/src/util/schema.d.ts +1 -1
  84. package/dist/types/src/util/schema.d.ts.map +1 -1
  85. package/dist/types/src/util/sort.d.ts +12 -0
  86. package/dist/types/src/util/sort.d.ts.map +1 -0
  87. package/dist/types/src/util/table-buttons.d.ts.map +1 -1
  88. package/dist/types/src/util/table-controls.d.ts.map +1 -1
  89. package/dist/types/tsconfig.tsbuildinfo +1 -1
  90. package/package.json +65 -64
  91. package/src/components/Table/ColumnActionsMenu.tsx +11 -7
  92. package/src/components/Table/ColumnSettings.tsx +7 -7
  93. package/src/components/Table/DynamicTable.stories.tsx +25 -22
  94. package/src/components/Table/DynamicTable.tsx +26 -21
  95. package/src/components/Table/Relations.stories.tsx +237 -0
  96. package/src/components/Table/RowActionsMenu.tsx +26 -17
  97. package/src/components/Table/Table.stories.tsx +110 -102
  98. package/src/components/Table/Table.tsx +353 -298
  99. package/src/components/TableCellEditor/CellValidationMessage.tsx +2 -2
  100. package/src/components/TableCellEditor/FormCellEditor.tsx +128 -40
  101. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +30 -66
  102. package/src/components/TableCellEditor/TableCellEditor.tsx +88 -238
  103. package/src/components/TableCellEditor/index.ts +1 -0
  104. package/src/components/TableToolbar/TableToolbar.tsx +49 -33
  105. package/src/components/testing.ts +39 -13
  106. package/src/hooks/index.ts +2 -1
  107. package/src/hooks/useAddRow.ts +14 -14
  108. package/src/hooks/useProjectionModel.ts +39 -0
  109. package/src/hooks/useTableModel.ts +35 -33
  110. package/src/model/modal-controller.ts +24 -41
  111. package/src/model/selection-model.ts +75 -43
  112. package/src/model/table-model.test.ts +92 -55
  113. package/src/model/table-model.ts +445 -168
  114. package/src/model/table-presentation.test.ts +70 -58
  115. package/src/model/table-presentation.ts +103 -57
  116. package/src/playwright/TableManager.ts +5 -5
  117. package/src/playwright/playwright.config.ts +1 -1
  118. package/src/playwright/smoke.spec.ts +135 -40
  119. package/src/testing/useTestTableModel.ts +45 -34
  120. package/src/translations.ts +3 -3
  121. package/src/types/Table.ts +90 -0
  122. package/src/types/index.ts +1 -1
  123. package/src/util/dynamic-table.ts +44 -32
  124. package/src/util/index.ts +2 -1
  125. package/src/util/query-ast.ts +19 -0
  126. package/src/util/schema.ts +4 -4
  127. package/src/util/sort.ts +90 -0
  128. package/src/util/table-buttons.ts +4 -3
  129. package/src/util/table-controls.ts +4 -1
  130. package/dist/lib/browser/chunk-5XLSHLOG.mjs +0 -1751
  131. package/dist/lib/browser/chunk-5XLSHLOG.mjs.map +0 -7
  132. package/dist/lib/browser/chunk-L2LSB6KV.mjs +0 -18
  133. package/dist/lib/browser/chunk-L2LSB6KV.mjs.map +0 -7
  134. package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs +0 -20
  135. package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs.map +0 -7
  136. package/dist/lib/node-esm/chunk-Y66YLSTC.mjs +0 -1752
  137. package/dist/lib/node-esm/chunk-Y66YLSTC.mjs.map +0 -7
  138. package/dist/types/src/components/Table/CreateRefPanel.d.ts +0 -9
  139. package/dist/types/src/components/Table/CreateRefPanel.d.ts.map +0 -1
  140. package/dist/types/src/components/Table/relations.stories.d.ts +0 -9
  141. package/dist/types/src/components/Table/relations.stories.d.ts.map +0 -1
  142. package/dist/types/src/components/TableCellEditor/extension.d.ts +0 -13
  143. package/dist/types/src/components/TableCellEditor/extension.d.ts.map +0 -1
  144. package/dist/types/src/model/table-sorting.d.ts +0 -53
  145. package/dist/types/src/model/table-sorting.d.ts.map +0 -1
  146. package/dist/types/src/types/schema.d.ts +0 -8
  147. package/dist/types/src/types/schema.d.ts.map +0 -1
  148. package/dist/types/src/util/table.d.ts +0 -11
  149. package/dist/types/src/util/table.d.ts.map +0 -1
  150. package/src/components/Table/CreateRefPanel.tsx +0 -86
  151. package/src/components/Table/relations.stories.tsx +0 -116
  152. package/src/components/TableCellEditor/extension.ts +0 -61
  153. package/src/model/table-sorting.ts +0 -216
  154. package/src/types/schema.ts +0 -16
  155. package/src/util/table.ts +0 -49
@@ -4,16 +4,20 @@
4
4
 
5
5
  import { expect, test } from '@playwright/test';
6
6
 
7
+ import { type DxGrid } from '@dxos/lit-grid';
8
+ import { faker } from '@dxos/random';
7
9
  import { setupPage, storybookUrl } from '@dxos/test-utils/playwright';
8
10
 
9
11
  import { TableManager } from './TableManager';
10
12
 
11
13
  const storyUrl = storybookUrl('ui-react-ui-table-table--default', 9004);
14
+ const relationsStoryUrl = storybookUrl('ui-react-ui-table-relations--default', 9004);
12
15
 
13
16
  // NOTE(ZaymonFC): This test suite relies on the faker seed being set to 0 in the story.
14
17
  test.describe('Table', () => {
15
18
  test('Loads', async ({ browser, browserName }) => {
16
19
  test.skip(browserName === 'webkit');
20
+ test.skip(browserName === 'firefox');
17
21
  const { page } = await setupPage(browser, { url: storyUrl });
18
22
  const table = new TableManager(page);
19
23
 
@@ -23,6 +27,7 @@ test.describe('Table', () => {
23
27
 
24
28
  test('sort', async ({ browser, browserName }) => {
25
29
  test.skip(browserName === 'webkit');
30
+ test.skip(browserName === 'firefox');
26
31
  const { page } = await setupPage(browser, { url: storyUrl });
27
32
  const table = new TableManager(page);
28
33
 
@@ -39,6 +44,7 @@ test.describe('Table', () => {
39
44
 
40
45
  test('selection', async ({ browser, browserName }) => {
41
46
  test.skip(browserName === 'webkit');
47
+ test.skip(browserName === 'firefox');
42
48
  const { page } = await setupPage(browser, { url: storyUrl });
43
49
  const table = new TableManager(page);
44
50
 
@@ -60,10 +66,12 @@ test.describe('Table', () => {
60
66
 
61
67
  test('delete row', async ({ browser, browserName }) => {
62
68
  test.skip(browserName === 'webkit');
69
+ test.skip(browserName === 'firefox');
63
70
  const { page } = await setupPage(browser, { url: storyUrl });
64
71
  const table = new TableManager(page);
65
72
 
66
73
  await table.grid.ready();
74
+ await expect(page.getByRole('gridcell', { name: 'Sapiente.' })).toHaveCount(1);
67
75
  await table.deleteRow(0);
68
76
  await expect(page.getByRole('gridcell', { name: 'Sapiente.' })).toHaveCount(0);
69
77
  await page.close();
@@ -71,6 +79,7 @@ test.describe('Table', () => {
71
79
 
72
80
  test('delete row--select all', async ({ browser, browserName }) => {
73
81
  test.skip(browserName === 'webkit');
82
+ test.skip(browserName === 'firefox');
74
83
  const { page } = await setupPage(browser, { url: storyUrl });
75
84
  const table = new TableManager(page);
76
85
 
@@ -80,7 +89,7 @@ test.describe('Table', () => {
80
89
  // Delete action affects all selected rows.
81
90
  await table.deleteRow(0);
82
91
 
83
- await expect(page.getByRole('gridcell', { name: 'Sapiente.' })).toHaveCount(0);
92
+ await expect(page.getByRole('gridcell', { name: 'Aut.' })).toHaveCount(0);
84
93
  await expect(page.getByRole('gridcell', { name: 'Beatae.' })).toHaveCount(0);
85
94
  await expect(table.grid.cellsWithinPlane('grid')).toHaveCount(0);
86
95
  await page.close();
@@ -88,6 +97,7 @@ test.describe('Table', () => {
88
97
 
89
98
  test('delete column', async ({ browser, browserName }) => {
90
99
  test.skip(browserName === 'webkit');
100
+ test.skip(browserName === 'firefox');
91
101
  const { page } = await setupPage(browser, { url: storyUrl });
92
102
  const table = new TableManager(page);
93
103
 
@@ -114,56 +124,22 @@ test.describe('Table', () => {
114
124
  // Rest of add column test remains the same as it's a more complex flow.
115
125
  test('add column', async ({ browser, browserName }) => {
116
126
  test.skip(browserName === 'webkit');
127
+ test.skip(browserName === 'firefox');
117
128
  const { page } = await setupPage(browser, { url: storyUrl });
118
129
  const table = new TableManager(page);
119
130
 
120
131
  await table.grid.ready();
121
132
  const newColumnLabel = 'TEST LABEL';
122
133
 
123
- await table.addColumn({ label: newColumnLabel, format: 'number' });
134
+ await table.addColumn({ label: newColumnLabel, format: 'Number' });
124
135
 
125
136
  await expect(page.getByRole('gridcell', { name: newColumnLabel })).toBeVisible();
126
137
  await page.close();
127
138
  });
128
139
 
129
- test('reference > reference / create new object', async ({ browser, browserName }) => {
130
- test.skip(browserName === 'webkit');
131
- const { page } = await setupPage(browser, { url: storyUrl });
132
- const table = new TableManager(page);
133
-
134
- await table.grid.ready();
135
-
136
- // Reference existing object.
137
- // Scroll over to the manager column.
138
- await table.grid.panByWheel(10000, 0);
139
-
140
- await table.grid.cell(4, 0, 'grid').click();
141
- // TODO(wittjosiah): Surprisingly long delay needed here.
142
- await page.keyboard.press('A', { delay: 1_000 });
143
- await page.keyboard.press('u', { delay: 1_000 });
144
- await page.getByRole('option', { name: 'Aut.' }).click();
145
-
146
- // Assert that the value is shown in the cell.
147
- await expect(page.getByRole('gridcell', { name: 'Aut.' }).first()).toBeVisible();
148
-
149
- // Create new object.
150
- await table.grid.cell(4, 1, 'grid').click();
151
- // TODO(wittjosiah): Surprisingly long delay needed here.
152
- await page.keyboard.type('t', { delay: 1_000 });
153
- await page.keyboard.type('e', { delay: 1_000 });
154
- await page.keyboard.type('s', { delay: 1_000 });
155
- await page.keyboard.type('t', { delay: 1_000 });
156
- await page.getByText('Create new object', { exact: false }).click();
157
- await page.getByTestId('save-button').click();
158
-
159
- // Scroll to the left.
160
- await page.getByRole('gridcell', { name: 'test' }).first().click();
161
-
162
- await page.close();
163
- });
164
-
165
- test('test toggles', async ({ browser, browserName }) => {
140
+ test.skip('test toggles', async ({ browser, browserName }) => {
166
141
  test.skip(browserName === 'webkit');
142
+ test.skip(browserName === 'firefox');
167
143
  const { page } = await setupPage(browser, { url: storyUrl });
168
144
  const table = new TableManager(page);
169
145
 
@@ -178,9 +154,128 @@ test.describe('Table', () => {
178
154
  // Assert the first two switch checkboxes are checked.
179
155
  await expect(page.getByTestId('table-switch').first()).toBeChecked();
180
156
  await expect(page.getByTestId('table-switch').nth(1)).toBeChecked();
181
- await expect(table.grid.cell(0, 0, 'grid')).toHaveText('Sapiente.');
157
+ await expect(table.grid.cell(0, 0, 'grid')).toHaveText('Aut.');
182
158
  await expect(table.grid.cell(0, 1, 'grid')).toHaveText('Beatae.');
183
159
 
184
160
  await page.close();
185
161
  });
162
+
163
+ // TODO(wittjosiah): Remove? Conflicts with story play function which is run as a unit test anyways.
164
+ test.skip('extant relations work as expected', async ({ browser, browserName }) => {
165
+ test.skip(browserName === 'webkit');
166
+ test.skip(browserName === 'firefox');
167
+ const { page } = await setupPage(browser, { url: relationsStoryUrl });
168
+
169
+ // Wait for the page to load
170
+ await page.locator('dx-grid > .dx-grid').nth(1).waitFor({ state: 'visible' });
171
+
172
+ // Find the dx-grid element for the contactModel (second table)
173
+ // The contactModel is used in the second Table.Main component in the story
174
+ const dxGrid = page.locator('dx-grid').nth(1);
175
+ await dxGrid.waitFor({ state: 'visible' });
176
+
177
+ // Scroll to the last column (column 4)
178
+ await dxGrid.evaluate(async (dxGridElement: DxGrid) => {
179
+ dxGridElement.scrollToColumn(4);
180
+ });
181
+
182
+ // Click on the cell at aria-rowindex=0 aria-colindex=4 to focus it
183
+ const targetCell = dxGrid.locator('[data-dx-grid-plane="grid"] [aria-rowindex="0"][aria-colindex="4"]');
184
+ await targetCell.click();
185
+
186
+ // Click again to engage edit mode
187
+ await page.keyboard.press('Enter');
188
+
189
+ // Click on the combobox to open options
190
+ await page.getByRole('combobox').click();
191
+ await page.pause();
192
+ await page.getByPlaceholder('Search…').focus();
193
+
194
+ // Type the first few letters of an org name.
195
+ const orgName =
196
+ (await page
197
+ .locator('dx-grid')
198
+ .nth(0)
199
+ .locator('[data-dx-grid-plane="grid"] [aria-rowindex="0"][aria-colindex="0"] .dx-grid__cell__content')
200
+ .textContent()) ?? 'never';
201
+ await page.keyboard.type(orgName.substring(0, 4), { delay: 500 });
202
+
203
+ // Assert that there is an element with aria-selected on the page
204
+ await expect(page.locator('[role="option"][aria-selected]')).toBeVisible();
205
+
206
+ // Type the enter key
207
+ await page.keyboard.press('Enter');
208
+
209
+ // Save the result
210
+ await page.getByTestId('save-button').click();
211
+
212
+ // Assert that the cell element has the org name
213
+ await expect(targetCell).toHaveText(orgName);
214
+
215
+ await page.close();
216
+ });
217
+
218
+ // TODO(wittjosiah): Remove? Conflicts with story play function which is run as a unit test anyways.
219
+ test.skip('new relations work as expected', async ({ browser, browserName }) => {
220
+ test.skip(browserName === 'webkit');
221
+ test.skip(browserName === 'firefox');
222
+ const { page } = await setupPage(browser, { url: relationsStoryUrl });
223
+
224
+ // Wait for the page to load
225
+ await page.locator('dx-grid > .dx-grid').nth(1).waitFor({ state: 'visible' });
226
+
227
+ // Find the dx-grid element for the contactModel (second table)
228
+ // The contactModel is used in the second Table.Main component in the story
229
+ const dxGrid = page.locator('dx-grid').nth(1);
230
+ await dxGrid.waitFor({ state: 'visible' });
231
+
232
+ // Scroll to the last column (column 4)
233
+ await dxGrid.evaluate(async (dxGridElement: DxGrid) => {
234
+ dxGridElement.scrollToColumn(4);
235
+ });
236
+
237
+ // Click on the cell at aria-rowindex=0 aria-colindex=8 to focus it
238
+ const targetCell = dxGrid.locator('[data-dx-grid-plane="grid"] [aria-rowindex="0"][aria-colindex="4"]');
239
+ await targetCell.click();
240
+
241
+ // Click again to engage edit mode
242
+ await page.keyboard.press('Enter');
243
+
244
+ // Click on the combobox to open options
245
+ await page.getByRole('combobox').click();
246
+ await page.getByPlaceholder('Search…').focus();
247
+
248
+ // Type the first few letters of an org name.
249
+ const orgName = 'Sally';
250
+ await page.keyboard.type(orgName, { delay: 500 });
251
+
252
+ // Assert that there is an element with aria-selected on the page
253
+ await expect(page.locator('[role="option"][aria-selected]')).toBeVisible();
254
+
255
+ // Type the enter key
256
+ await page.keyboard.press('Enter');
257
+ await page.pause();
258
+
259
+ // Click the save button in the popover
260
+ await page.getByTestId('create-referenced-object-form').getByTestId('save-button').click();
261
+
262
+ // Assert that the cell element has the org name
263
+ await expect(targetCell).toHaveText(orgName);
264
+
265
+ // Make a change to a non-ref cell to check that populated refs don’t cause problems with snapshots or schemas.
266
+ await dxGrid.evaluate(async (dxGridElement: DxGrid) => {
267
+ dxGridElement.scrollToColumn(6);
268
+ });
269
+
270
+ const nonRefContent = faker.lorem.words(3);
271
+ const nonRefCell = dxGrid.locator('[data-dx-grid-plane="grid"] [aria-rowindex="0"][aria-colindex="6"]');
272
+ await nonRefCell.click();
273
+ await page.keyboard.press('Enter');
274
+ await page.getByTestId('grid.cell-editor').waitFor({ state: 'visible' });
275
+ await page.keyboard.type(nonRefContent, { delay: 500 });
276
+ await page.keyboard.press('Enter');
277
+ await expect(nonRefCell).toHaveText(nonRefContent);
278
+
279
+ await page.close();
280
+ });
186
281
  });
@@ -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';
8
+ import { type Database, type Type } from '@dxos/echo';
9
+ import { isMutable } from '@dxos/echo/internal';
9
10
  import { faker } from '@dxos/random';
10
- import { useClient } from '@dxos/react-client';
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-searchlist';
14
+ import { type ProjectionModel, getTypenameFromQuery } from '@dxos/schema';
14
15
 
15
16
  import { type TableController } from '../components';
16
- import { useTableModel, useAddRow } from '../hooks';
17
- import { TablePresentation } from '../model';
17
+ import { useAddRow, useProjectionModel, useTableModel } from '../hooks';
18
+ import { type TableModel, TablePresentation } from '../model';
19
+ import { Table } from '../types';
18
20
 
19
21
  faker.seed(0); // NOTE(ZaymonFC): Required for smoke tests.
20
22
 
23
+ export type TestTableModel<T extends Type.Obj.Any = Type.Obj.Any> = {
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
+ };
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.Obj.Any = Type.Obj.Any>(): 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,8 +91,7 @@ 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,
@@ -92,7 +103,8 @@ export const useTestTableModel = () => {
92
103
  });
93
104
 
94
105
  const handleInsertRow = useCallback(() => {
95
- model?.insertRow();
106
+ const insertResult = model?.insertRow();
107
+ tableRef.current?.handleInsertRowResult?.(insertResult);
96
108
  }, [model]);
97
109
 
98
110
  const handleSaveView = useCallback(() => {
@@ -101,19 +113,18 @@ export const useTestTableModel = () => {
101
113
 
102
114
  const presentation = useMemo(() => {
103
115
  if (model) {
104
- return new TablePresentation(model);
116
+ return new TablePresentation(registry, model);
105
117
  }
106
- }, [model]);
118
+ }, [registry, model]);
107
119
 
108
120
  return {
109
121
  schema,
110
- view,
122
+ table,
111
123
  projection,
112
124
  tableRef,
113
125
  model,
114
126
  presentation,
115
- space,
116
- client,
127
+ db,
117
128
  handleInsertRow,
118
129
  handleSaveView,
119
130
  handleDeleteRows,
@@ -4,7 +4,7 @@
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
  {
@@ -14,8 +14,8 @@ export const translations = [
14
14
  'create object label': 'Create table',
15
15
  'table name placeholder': 'Table name',
16
16
  'settings title': 'Table settings',
17
- 'table schema label': 'Schema',
18
- 'new schema': 'New schema',
17
+ 'table schema label': 'Type',
18
+ 'new schema': 'New type',
19
19
  'continue label': 'Continue',
20
20
  'create stack section label': 'Create table',
21
21
  'add row label': 'Add row',
@@ -0,0 +1,90 @@
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 { Obj, Ref, Type } from '@dxos/echo';
10
+ import { FormInputAnnotation, type JsonPath, type JsonSchemaType, LabelAnnotation } from '@dxos/echo/internal';
11
+ import { View, ViewAnnotation } from '@dxos/schema';
12
+
13
+ export const Table = Schema.Struct({
14
+ name: Schema.String.pipe(Schema.optional),
15
+
16
+ view: Type.Ref(View.View).pipe(FormInputAnnotation.set(false)),
17
+
18
+ sizes: Schema.Record({
19
+ // TODO(wittjosiah): Should be JsonPath.
20
+ key: Schema.String,
21
+ value: Schema.Number,
22
+ }).pipe(Schema.mutable, FormInputAnnotation.set(false)),
23
+ }).pipe(
24
+ Type.object({
25
+ typename: 'dxos.org/type/Table',
26
+ version: '0.2.0',
27
+ }),
28
+ LabelAnnotation.set(['name']),
29
+ ViewAnnotation.set(true),
30
+ );
31
+
32
+ export interface Table extends Schema.Schema.Type<typeof Table> {}
33
+
34
+ type MakeProps = {
35
+ name?: string;
36
+ sizes?: Record<string, number>;
37
+ view: View.View;
38
+ /** Required to auto-size columns. */
39
+ jsonSchema?: JsonSchemaType;
40
+ };
41
+
42
+ /**
43
+ * Make a table as a view of a data set.
44
+ */
45
+ export const make = ({ name, sizes = {}, view, jsonSchema }: MakeProps): Table => {
46
+ const table = Obj.make(Table, { name, view: Ref.make(view), sizes } as Obj.MakeProps<typeof Table>);
47
+
48
+ // Preset sizes.
49
+ if (jsonSchema) {
50
+ const schema = Type.toEffectSchema(jsonSchema);
51
+ const properties = SchemaAST.getPropertySignatures(schema.ast);
52
+ for (const property of properties) {
53
+ const name = property.name.toString() as JsonPath;
54
+ if (sizes?.[name]) {
55
+ table.sizes[name] = sizes[name];
56
+ continue;
57
+ }
58
+
59
+ Match.type<SchemaAST.AST>().pipe(
60
+ Match.when({ _tag: 'BooleanKeyword' }, () => {
61
+ table.sizes[name] = 100;
62
+ }),
63
+ Match.when({ _tag: 'NumberKeyword' }, () => {
64
+ table.sizes[name] = 100;
65
+ }),
66
+ Match.orElse(() => {
67
+ // Noop.
68
+ }),
69
+ )(property.type);
70
+ }
71
+ }
72
+
73
+ return table;
74
+ };
75
+
76
+ //
77
+ // V1
78
+ //
79
+
80
+ export const TableV1 = Schema.Struct({
81
+ sizes: Schema.Record({
82
+ key: Schema.String,
83
+ value: Schema.Number,
84
+ }).pipe(Schema.mutable),
85
+ }).pipe(
86
+ Type.object({
87
+ typename: 'dxos.org/type/Table',
88
+ version: '0.1.0',
89
+ }),
90
+ );
@@ -2,4 +2,4 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- export * from './schema';
5
+ export * as Table from './Table';
@@ -2,26 +2,24 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { type Schema } from 'effect';
5
+ import { type Registry } from '@effect-atom/atom-react';
6
6
 
7
- import { Obj } from '@dxos/echo';
8
- import { getTypename, toJsonSchema } from '@dxos/echo-schema';
9
- import type { JsonSchemaType, SortDirectionType } from '@dxos/echo-schema';
7
+ import { Filter, type JsonSchema, Obj, Order, Query, type QueryAST, Ref, Type } from '@dxos/echo';
10
8
  import {
11
- createView,
12
- type DataType,
13
- getSchemaFromPropertyDefinitions,
14
9
  ProjectionModel,
15
10
  type SchemaPropertyDefinition,
11
+ View,
12
+ createDirectChangeCallback,
13
+ getSchemaFromPropertyDefinitions,
16
14
  } from '@dxos/schema';
17
15
 
18
- import { TableView } from '../types';
16
+ import { Table } from '../types';
19
17
 
20
18
  // TODO(ZaymonFC): Upstream these extra fields to SchemaPropertyDefinition to enhance schema-tools schema creation.
21
19
  type PropertyDisplayProps = {
22
20
  size: number;
23
21
  title: string;
24
- sort: SortDirectionType;
22
+ sort: QueryAST.OrderDirection;
25
23
  };
26
24
 
27
25
  export type TablePropertyDefinition = SchemaPropertyDefinition & Partial<PropertyDisplayProps>;
@@ -31,21 +29,21 @@ export type TablePropertyDefinition = SchemaPropertyDefinition & Partial<Propert
31
29
  */
32
30
  // TODO(burdon): Remove variance.
33
31
  export const getBaseSchema = ({
32
+ schema,
34
33
  typename,
35
34
  properties,
36
35
  jsonSchema,
37
- schema,
38
36
  }: {
37
+ schema?: Type.Entity.Any;
39
38
  typename?: string;
40
39
  properties?: TablePropertyDefinition[];
41
- jsonSchema?: JsonSchemaType;
42
- schema?: Schema.Schema.AnyNoContext;
43
- }): { typename: string; jsonSchema: JsonSchemaType } => {
40
+ jsonSchema?: JsonSchema.JsonSchema;
41
+ }): { typename: string; jsonSchema: JsonSchema.JsonSchema } => {
44
42
  if (typename && properties) {
45
43
  const schema = getSchemaFromPropertyDefinitions(typename, properties);
46
44
  return { typename: schema.typename, jsonSchema: schema.jsonSchema };
47
45
  } else if (schema) {
48
- return { typename: getTypename(schema)!, jsonSchema: toJsonSchema(schema) };
46
+ return { typename: Type.getTypename(schema)!, jsonSchema: Type.toJsonSchema(schema) };
49
47
  } else if (typename && jsonSchema) {
50
48
  return { typename, jsonSchema };
51
49
  } else {
@@ -54,41 +52,49 @@ export const getBaseSchema = ({
54
52
  };
55
53
 
56
54
  export const makeDynamicTable = ({
57
- typename,
55
+ registry,
58
56
  jsonSchema,
59
57
  properties,
60
58
  }: {
61
- typename: string;
62
- jsonSchema: JsonSchemaType;
59
+ registry: Registry.Registry;
60
+ jsonSchema: JsonSchema.JsonSchema;
63
61
  properties?: TablePropertyDefinition[];
64
- }): { projection: ProjectionModel; view: DataType.View } => {
65
- const table = Obj.make(TableView, { sizes: {} });
66
- const view = createView({
67
- typename,
62
+ }): { projection: ProjectionModel; object: Table.Table } => {
63
+ const view = View.make({
64
+ query: Query.select(Filter.everything()),
68
65
  jsonSchema,
69
- presentation: table,
70
66
  ...(properties && { fields: properties.map((property) => property.name) }),
71
67
  });
68
+ const object = Obj.make(Table.Table, { view: Ref.make(view), sizes: {} });
72
69
 
73
- const projection = new ProjectionModel(jsonSchema, view.projection);
74
- if (properties && projection.fields) {
75
- setProperties(view, projection, table, properties);
70
+ // Use direct change callback for in-memory objects (non-ECHO backed).
71
+ const projection = new ProjectionModel({
72
+ registry,
73
+ view,
74
+ baseSchema: jsonSchema,
75
+ change: createDirectChangeCallback(view.projection, jsonSchema),
76
+ });
77
+ projection.normalizeView();
78
+ if (properties && projection.getFields()) {
79
+ setProperties(view, projection, object, properties);
76
80
  }
77
81
 
78
- return { projection, view };
82
+ return { projection, object };
79
83
  };
80
84
 
81
85
  const setProperties = (
82
- view: DataType.View,
86
+ view: View.View,
83
87
  projection: ProjectionModel,
84
- table: TableView,
88
+ table: Table.Table,
85
89
  properties: TablePropertyDefinition[],
86
90
  ) => {
87
91
  for (const property of properties) {
88
- const field = projection.fields.find((field) => field.path === property.name);
92
+ const field = projection.getFields().find((field) => field.path === property.name);
89
93
  if (field) {
90
94
  if (property.size !== undefined) {
91
- table.sizes[field.path] = property.size;
95
+ Obj.change(table, (t) => {
96
+ t.sizes[field.path] = property.size!;
97
+ });
92
98
  }
93
99
 
94
100
  if (property.title !== undefined) {
@@ -100,8 +106,14 @@ const setProperties = (
100
106
  }
101
107
 
102
108
  if (property.sort) {
103
- const fieldId = field.id;
104
- view.query.sort = [{ fieldId, direction: property.sort }];
109
+ // Apply sort to query instead of deprecated view.sort
110
+ const currentQuery = Query.fromAst(Obj.getSnapshot(view).query.ast);
111
+ // Use any type parameter since we're working with dynamic field paths
112
+ const newQuery = currentQuery.orderBy(Order.property<any>(field.path as string, property.sort));
113
+ Obj.change(view, (v) => {
114
+ // Type assertion needed because Query AST types have some variance issues.
115
+ v.query.ast = newQuery.ast as typeof v.query.ast;
116
+ });
105
117
  }
106
118
  }
107
119
  }
package/src/util/index.ts CHANGED
@@ -3,8 +3,9 @@
3
3
  //
4
4
 
5
5
  export * from './dynamic-table';
6
+ export * from './query-ast';
6
7
  export * from './signals';
7
- export * from './table';
8
+ export * from './sort';
8
9
  export * from './table-buttons';
9
10
  export * from './table-controls';
10
11
  export * from './schema';