@dxos/react-ui-table 0.8.4-main.2e9d522 → 0.8.4-main.3c1ae3b

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 (144) hide show
  1. package/dist/lib/browser/chunk-CJQ6T4LL.mjs +77 -0
  2. package/dist/lib/browser/chunk-CJQ6T4LL.mjs.map +7 -0
  3. package/dist/lib/browser/{chunk-5XLSHLOG.mjs → chunk-V7A7ZA5E.mjs} +576 -544
  4. package/dist/lib/browser/chunk-V7A7ZA5E.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +694 -760
  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 +16 -27
  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-Y66YLSTC.mjs → chunk-P73ALX3N.mjs} +576 -544
  12. package/dist/lib/node-esm/chunk-P73ALX3N.mjs.map +7 -0
  13. package/dist/lib/node-esm/chunk-T6PXZQ6W.mjs +78 -0
  14. package/dist/lib/node-esm/chunk-T6PXZQ6W.mjs.map +7 -0
  15. package/dist/lib/node-esm/index.mjs +694 -760
  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 +16 -27
  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 +14 -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 +2 -2
  48. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -1
  49. package/dist/types/src/components/testing.d.ts +3 -5
  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 +3 -2
  54. package/dist/types/src/hooks/useAddRow.d.ts.map +1 -1
  55. package/dist/types/src/hooks/useProjectionModel.d.ts +5 -0
  56. package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -0
  57. package/dist/types/src/hooks/useTableModel.d.ts +5 -6
  58. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  59. package/dist/types/src/model/modal-controller.d.ts +0 -7
  60. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  61. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  62. package/dist/types/src/model/table-model.d.ts +14 -14
  63. package/dist/types/src/model/table-model.d.ts.map +1 -1
  64. package/dist/types/src/model/table-presentation.d.ts +2 -1
  65. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  66. package/dist/types/src/model/table-sorting.d.ts +9 -6
  67. package/dist/types/src/model/table-sorting.d.ts.map +1 -1
  68. package/dist/types/src/playwright/TableManager.d.ts +1 -1
  69. package/dist/types/src/playwright/TableManager.d.ts.map +1 -1
  70. package/dist/types/src/testing/useTestTableModel.d.ts +19 -34
  71. package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -1
  72. package/dist/types/src/translations.d.ts +2 -2
  73. package/dist/types/src/types/Table.d.ts +28 -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 +10 -11
  78. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  79. package/dist/types/src/util/index.d.ts +0 -1
  80. package/dist/types/src/util/index.d.ts.map +1 -1
  81. package/dist/types/src/util/schema.d.ts +1 -1
  82. package/dist/types/src/util/schema.d.ts.map +1 -1
  83. package/dist/types/src/util/table-buttons.d.ts.map +1 -1
  84. package/dist/types/src/util/table-controls.d.ts.map +1 -1
  85. package/dist/types/tsconfig.tsbuildinfo +1 -1
  86. package/package.json +57 -51
  87. package/src/components/Table/ColumnActionsMenu.tsx +5 -2
  88. package/src/components/Table/ColumnSettings.tsx +3 -4
  89. package/src/components/Table/DynamicTable.stories.tsx +21 -19
  90. package/src/components/Table/DynamicTable.tsx +24 -18
  91. package/src/components/Table/Relations.stories.tsx +233 -0
  92. package/src/components/Table/RowActionsMenu.tsx +23 -15
  93. package/src/components/Table/Table.stories.tsx +104 -99
  94. package/src/components/Table/Table.tsx +338 -295
  95. package/src/components/TableCellEditor/CellValidationMessage.tsx +2 -2
  96. package/src/components/TableCellEditor/FormCellEditor.tsx +143 -39
  97. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +30 -66
  98. package/src/components/TableCellEditor/TableCellEditor.tsx +86 -236
  99. package/src/components/TableCellEditor/index.ts +1 -0
  100. package/src/components/TableToolbar/TableToolbar.tsx +34 -24
  101. package/src/components/testing.ts +4 -4
  102. package/src/hooks/index.ts +2 -1
  103. package/src/hooks/useAddRow.ts +7 -5
  104. package/src/hooks/useProjectionModel.ts +19 -0
  105. package/src/hooks/useTableModel.ts +13 -15
  106. package/src/model/modal-controller.ts +1 -29
  107. package/src/model/selection-model.ts +3 -2
  108. package/src/model/table-model.test.ts +21 -12
  109. package/src/model/table-model.ts +64 -58
  110. package/src/model/table-presentation.test.ts +25 -23
  111. package/src/model/table-presentation.ts +82 -41
  112. package/src/model/table-sorting.ts +32 -14
  113. package/src/playwright/TableManager.ts +5 -5
  114. package/src/playwright/smoke.spec.ts +132 -38
  115. package/src/testing/useTestTableModel.ts +32 -24
  116. package/src/translations.ts +2 -2
  117. package/src/types/Table.ts +90 -0
  118. package/src/types/index.ts +1 -1
  119. package/src/util/dynamic-table.ts +21 -27
  120. package/src/util/index.ts +0 -1
  121. package/src/util/schema.ts +4 -4
  122. package/src/util/table-buttons.ts +4 -3
  123. package/src/util/table-controls.ts +4 -1
  124. package/dist/lib/browser/chunk-5XLSHLOG.mjs.map +0 -7
  125. package/dist/lib/browser/chunk-L2LSB6KV.mjs +0 -18
  126. package/dist/lib/browser/chunk-L2LSB6KV.mjs.map +0 -7
  127. package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs +0 -20
  128. package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs.map +0 -7
  129. package/dist/lib/node-esm/chunk-Y66YLSTC.mjs.map +0 -7
  130. package/dist/types/src/components/Table/CreateRefPanel.d.ts +0 -9
  131. package/dist/types/src/components/Table/CreateRefPanel.d.ts.map +0 -1
  132. package/dist/types/src/components/Table/relations.stories.d.ts +0 -9
  133. package/dist/types/src/components/Table/relations.stories.d.ts.map +0 -1
  134. package/dist/types/src/components/TableCellEditor/extension.d.ts +0 -13
  135. package/dist/types/src/components/TableCellEditor/extension.d.ts.map +0 -1
  136. package/dist/types/src/types/schema.d.ts +0 -8
  137. package/dist/types/src/types/schema.d.ts.map +0 -1
  138. package/dist/types/src/util/table.d.ts +0 -11
  139. package/dist/types/src/util/table.d.ts.map +0 -1
  140. package/src/components/Table/CreateRefPanel.tsx +0 -86
  141. package/src/components/Table/relations.stories.tsx +0 -116
  142. package/src/components/TableCellEditor/extension.ts +0 -61
  143. package/src/types/schema.ts +0 -16
  144. package/src/util/table.ts +0 -49
@@ -3,10 +3,10 @@
3
3
  //
4
4
 
5
5
  import { signal } from '@preact/signals-core';
6
- import { isNotNullable } from 'effect/Predicate';
6
+ import * as Predicate from 'effect/Predicate';
7
7
 
8
8
  import { Obj } from '@dxos/echo';
9
- import { FormatEnum, getValue, TypeEnum } from '@dxos/echo-schema';
9
+ import { Format, TypeEnum, getValue } from '@dxos/echo/internal';
10
10
  import { cellClassesForFieldType, formatForDisplay } from '@dxos/react-ui-form';
11
11
  import {
12
12
  type DxGridCellValue,
@@ -16,12 +16,13 @@ import {
16
16
  toPlaneCellIndex,
17
17
  } from '@dxos/react-ui-grid';
18
18
  import { mx } from '@dxos/react-ui-theme';
19
- import { VIEW_FIELD_LIMIT, type FieldType } from '@dxos/schema';
19
+ import { type FieldType, VIEW_FIELD_LIMIT } from '@dxos/schema';
20
20
 
21
- import { type SelectionMode } from './selection-model';
22
- import { type TableRow, type TableModel } from './table-model';
23
21
  import { tableButtons, tableControls } from '../util';
24
22
 
23
+ import { type SelectionMode } from './selection-model';
24
+ import { type TableModel, type TableRow } from './table-model';
25
+
25
26
  /**
26
27
  * Presentation layer for a table component, handling cell rendering and grid display logic.
27
28
  * Manages rendering of table cells, headers, selection columns, and action columns across
@@ -67,6 +68,9 @@ export class TablePresentation<T extends TableRow = TableRow> {
67
68
  case 'frozenRowsEnd':
68
69
  cells = this.getDraftRowCells(range);
69
70
  break;
71
+ case 'fixedEndStart':
72
+ cells = this.getDraftIconCells(range);
73
+ break;
70
74
  case 'fixedEndEnd':
71
75
  cells = this.getDraftActionCells(range);
72
76
  break;
@@ -116,12 +120,12 @@ export class TablePresentation<T extends TableRow = TableRow> {
116
120
  }
117
121
 
118
122
  switch (props.format) {
119
- case FormatEnum.Boolean:
120
- case FormatEnum.SingleSelect:
121
- case FormatEnum.MultiSelect: {
123
+ case Format.TypeFormat.Boolean:
124
+ case Format.TypeFormat.SingleSelect:
125
+ case Format.TypeFormat.MultiSelect: {
122
126
  return '';
123
127
  }
124
- case FormatEnum.Ref: {
128
+ case Format.TypeFormat.Ref: {
125
129
  if (!field.referencePath) {
126
130
  return ''; // TODO(burdon): Show error.
127
131
  }
@@ -137,20 +141,22 @@ export class TablePresentation<T extends TableRow = TableRow> {
137
141
  };
138
142
 
139
143
  const classes = [];
140
- const formatClasses = cellClassesForFieldType({ type: props.type, format: props.format });
144
+ const formatClasses = cellClassesForFieldType(props);
145
+
141
146
  if (formatClasses) {
142
147
  classes.push(formatClasses);
143
148
  }
149
+
144
150
  const rowSelectionClasses = cellClassesForRowSelection(
145
151
  this.model.selection.isObjectSelected(obj),
146
152
  this.model.selection.selectionMode,
147
153
  );
154
+
148
155
  if (rowSelectionClasses) {
149
156
  classes.push(rowSelectionClasses);
150
157
  }
151
- if (classes.length > 0) {
152
- cell.className = mx(classes.flat());
153
- }
158
+
159
+ cell.className = mx(classes);
154
160
 
155
161
  // Arrays.
156
162
  if (props.type === TypeEnum.Array) {
@@ -184,7 +190,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
184
190
 
185
191
  const tags = targetArray
186
192
  .map(getLabel)
187
- .filter(isNotNullable)
193
+ .filter(Predicate.isNotNullable)
188
194
  .map((title) => {
189
195
  return `<span class="dx-tag" data-hue="neutral">${title}</span>`;
190
196
  })
@@ -195,16 +201,16 @@ export class TablePresentation<T extends TableRow = TableRow> {
195
201
  }
196
202
 
197
203
  // References.
198
- if (props.format === FormatEnum.Ref && props.referenceSchema) {
204
+ if (props.format === Format.TypeFormat.Ref && props.referenceSchema) {
199
205
  const targetObj = getValue(obj, field.path)?.target;
200
206
  if (targetObj) {
201
207
  const dxn = Obj.getDXN(targetObj)?.toString();
202
- cell.accessoryHtml = `<dx-ref-tag refId=${dxn} class="dx-button is-6 pli-[3px] pbe-[2px] min-bs-0 absolute inline-end-1"><dx-icon icon="ph--link-simple--regular"/></dx-ref-tag>`;
208
+ cell.accessoryHtml = `<div role="none" class="absolute inline-end-0 inset-block-0 p-[--dx-grid-cell-content-padding-block]"><dx-anchor refId=${dxn} class="dx-button is-6 aspect-square min-bs-0" data-dx-grid-action="accessory"><dx-icon icon="ph--link-simple--regular"/></dx-anchor></div>`;
203
209
  }
204
210
  }
205
211
 
206
212
  // Booleans.
207
- if (props.format === FormatEnum.Boolean) {
213
+ if (props.format === Format.TypeFormat.Boolean) {
208
214
  const value = getValue(obj, field.path);
209
215
  cell.accessoryHtml = tableControls.switch.render({
210
216
  colIndex,
@@ -215,7 +221,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
215
221
  }
216
222
 
217
223
  // Single-Selects.
218
- if (props.format === FormatEnum.SingleSelect) {
224
+ if (props.format === Format.TypeFormat.SingleSelect) {
219
225
  const value = getValue(obj, field.path);
220
226
  const options = this.model.projection.getFieldProjection(field.id).props.options;
221
227
  if (options) {
@@ -227,7 +233,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
227
233
  }
228
234
 
229
235
  // Multi-Selects.
230
- if (props.format === FormatEnum.MultiSelect) {
236
+ if (props.format === Format.TypeFormat.MultiSelect) {
231
237
  const values = getValue(obj, field.path) as string[] | undefined;
232
238
  const options = this.model.projection.getFieldProjection(field.id).props.options;
233
239
  if (options && values && values.length > 0) {
@@ -275,23 +281,34 @@ export class TablePresentation<T extends TableRow = TableRow> {
275
281
  const fields = this.model.projection?.fields ?? [];
276
282
  const draftRows = this.model.draftRows.value;
277
283
 
278
- for (let row = range.start.row; row <= range.end.row && row < draftRows.length; row++) {
279
- const draftRow = draftRows[row];
284
+ // Return cells of the CTA row if no draft row is active
285
+ if (draftRows.length === 0) {
280
286
  for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
281
- const field = fields[col];
282
- if (!field) {
283
- continue;
284
- }
287
+ cells[toPlaneCellIndex({ col, row: 0 })] = {
288
+ value: '',
289
+ readonly: true,
290
+ className: 'dx-grid__row--cta__cell',
291
+ };
292
+ }
293
+ } else {
294
+ for (let row = range.start.row; row <= range.end.row && row < draftRows.length; row++) {
295
+ const draftRow = draftRows[row];
296
+ for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
297
+ const cellIndex = toPlaneCellIndex({ col, row });
298
+ const field = fields[col];
299
+ if (!field) {
300
+ continue;
301
+ }
285
302
 
286
- this.createDataCell(cells, draftRow.data, field, col, row);
303
+ this.createDataCell(cells, draftRow.data, field, col, row);
287
304
 
288
- if (this.model.hasDraftRowValidationError(row, field.path)) {
289
- const cellIndex = toPlaneCellIndex({ col, row });
290
- const cellValue = cells[cellIndex];
291
- if (cellValue) {
292
- const existingClasses = cellValue.className || '';
293
- const draftClasses = 'dx-grid__cell--flagged';
294
- cellValue.className = existingClasses ? `${existingClasses} ${draftClasses}` : draftClasses;
305
+ if (this.model.hasDraftRowValidationError(row, field.path)) {
306
+ const cellValue = cells[cellIndex];
307
+ if (cellValue) {
308
+ const existingClasses = cellValue.className || '';
309
+ const draftClasses = 'dx-grid__cell--flagged';
310
+ cellValue.className = existingClasses ? `${existingClasses} ${draftClasses}` : draftClasses;
311
+ }
295
312
  }
296
313
  }
297
314
  }
@@ -310,14 +327,14 @@ export class TablePresentation<T extends TableRow = TableRow> {
310
327
 
311
328
  cells[toPlaneCellIndex({ col, row: 0 })] = {
312
329
  // TODO(burdon): Use same logic as form for fallback title.
313
- value: props.title ?? field.path,
314
- readonly: true,
330
+ value: '',
315
331
  resizeHandle: 'col',
316
332
  accessoryHtml: `
333
+ <span class="grow min-is-0 truncate">${props.title ?? field.path}</span>
317
334
  ${direction !== undefined ? tableButtons.sort.render({ fieldId: field.id, direction }) : ''}
318
335
  ${tableButtons.columnSettings.render({ fieldId: field.id })}
319
336
  `,
320
- className: '!bg-toolbarSurface',
337
+ className: '!bg-toolbarSurface !text-description [&>div]:flex [&>div]:items-stretch',
321
338
  };
322
339
  }
323
340
 
@@ -400,14 +417,39 @@ export class TablePresentation<T extends TableRow = TableRow> {
400
417
  const cells: DxGridPlaneCells = {};
401
418
  const draftRows = this.model.draftRows.value;
402
419
 
403
- for (let row = range.start.row; row <= range.end.row && row < draftRows.length; row++) {
404
- const draftRow = draftRows[row];
405
- const disabled = !draftRow.valid;
420
+ // Return cells of the CTA row if no draft row is active
421
+ if (draftRows.length === 0) {
422
+ cells[toPlaneCellIndex({ col: 0, row: 0 })] = {
423
+ value: '',
424
+ className: 'dx-grid__row--cta__cell',
425
+ readonly: true,
426
+ };
427
+ } else {
428
+ for (let row = range.start.row; row <= range.end.row && row < draftRows.length; row++) {
429
+ const draftRow = draftRows[row];
430
+ const disabled = !draftRow.valid;
431
+
432
+ cells[toPlaneCellIndex({ col: 0, row })] = {
433
+ value: '',
434
+ readonly: true,
435
+ accessoryHtml: tableButtons.saveDraftRow.render({ rowIndex: row, disabled }),
436
+ };
437
+ }
438
+ }
439
+
440
+ return cells;
441
+ }
442
+
443
+ private getDraftIconCells(range: DxGridPlaneRange): DxGridPlaneCells {
444
+ const cells: DxGridPlaneCells = {};
445
+ const draftRows = this.model.draftRows.value;
406
446
 
447
+ for (let row = range.start.row; row <= range.end.row; row++) {
407
448
  cells[toPlaneCellIndex({ col: 0, row })] = {
408
449
  value: '',
409
450
  readonly: true,
410
- accessoryHtml: tableButtons.saveDraftRow.render({ rowIndex: row, disabled }),
451
+ accessoryHtml: '<dx-icon icon="ph--plus--regular" class="contents"></dx-icon>',
452
+ className: mx('[&>div]:grid [&>div]:place-content-center', draftRows.length < 1 && 'dx-grid__row--cta__cell'),
411
453
  };
412
454
  }
413
455
 
@@ -426,7 +468,6 @@ export const cellClassesForRowSelection = (selected: boolean, selectionMode: Sel
426
468
 
427
469
  switch (selectionMode) {
428
470
  case 'single':
429
- // TODO(ZaymonFC): @thure, do we need a grid version of 'currentRelated'?
430
471
  return ['!bg-currentRelated dx-grid__cell--no-focus-unfurl hover:bg-hoverSurface !cursor-pointer'];
431
472
  case 'multiple':
432
473
  return ['!bg-gridCellSelected'];
@@ -2,15 +2,24 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { computed, signal, type Signal, type ReadonlySignal } from '@preact/signals-core';
5
+ import { type ReadonlySignal, type Signal, computed, signal } from '@preact/signals-core';
6
6
  import orderBy from 'lodash.orderby';
7
7
 
8
- import { getValue, FormatEnum, TypeEnum, type SortDirectionType, type FieldSortType } from '@dxos/echo-schema';
8
+ import { Format, TypeEnum, getValue } from '@dxos/echo/internal';
9
9
  import { formatForDisplay } from '@dxos/react-ui-form';
10
- import type { PropertyType, FieldType, DataType, ProjectionModel } from '@dxos/schema';
10
+ import {
11
+ type FieldSortType,
12
+ type FieldType,
13
+ type ProjectionModel,
14
+ type PropertyType,
15
+ type SortDirectionType,
16
+ type View,
17
+ } from '@dxos/schema';
11
18
 
12
19
  import { type TableRow } from './table-model';
13
20
 
21
+ // TODO(wittjosiah): Remove. Use ordering in queries instead.
22
+
14
23
  /**
15
24
  * Represents the local sort state.
16
25
  * - undefined: No local sort set, fallback to view sort.
@@ -43,19 +52,27 @@ type LocalSort = { type: 'cleared' } | { type: 'active'; sort: FieldSortType } |
43
52
  export class TableSorting<T extends TableRow> {
44
53
  private readonly _displayToDataIndex = new Map<number, number>();
45
54
  private readonly _rows: Signal<T[]>;
55
+ private readonly _getView: () => View.View;
56
+ private readonly _projection: ProjectionModel;
46
57
  private readonly _localSort = signal<LocalSort>(undefined);
47
58
  private readonly _isDirty: ReadonlySignal<boolean>;
48
59
  public readonly sortedRows: ReadonlySignal<T[]>;
49
60
 
50
- constructor(
51
- rows: Signal<T[]>,
52
- private readonly _view: DataType.View,
53
- private readonly _projection: ProjectionModel,
54
- ) {
61
+ constructor({
62
+ rows,
63
+ projection,
64
+ getView,
65
+ }: {
66
+ rows: Signal<T[]>;
67
+ getView: () => View.View;
68
+ projection: ProjectionModel;
69
+ }) {
55
70
  this._rows = rows;
71
+ this._getView = getView;
72
+ this._projection = projection;
56
73
  this._isDirty = computed(() => {
57
74
  const local = this._localSort.value;
58
- const viewSort = this._view.query.sort?.[0];
75
+ const viewSort = this._getView().sort?.[0];
59
76
  if (local?.type === 'cleared') {
60
77
  return viewSort !== undefined;
61
78
  }
@@ -83,7 +100,7 @@ export class TableSorting<T extends TableRow> {
83
100
  return local.sort;
84
101
  }
85
102
 
86
- return this._view.query.sort?.[0];
103
+ return this._getView().sort?.[0];
87
104
  }
88
105
 
89
106
  /**
@@ -128,10 +145,11 @@ export class TableSorting<T extends TableRow> {
128
145
 
129
146
  public save(): void {
130
147
  if (this._localSort.value !== undefined) {
148
+ const view = this._getView();
131
149
  if (this._localSort.value.type === 'active') {
132
- this._view.query.sort = [this._localSort.value.sort];
150
+ view.sort = [this._localSort.value.sort];
133
151
  } else {
134
- this._view.query.sort = [];
152
+ view.sort = [];
135
153
  }
136
154
  this._localSort.value = undefined;
137
155
  }
@@ -193,7 +211,7 @@ export class TableSorting<T extends TableRow> {
193
211
  * For single select fields, the sort value is determined by the index of the option in the
194
212
  * options array. This ensures sort order matches the configured option order.
195
213
  */
196
- if (format === FormatEnum.SingleSelect) {
214
+ if (format === Format.TypeFormat.SingleSelect) {
197
215
  const options = props.options;
198
216
  if (!options) {
199
217
  return undefined;
@@ -206,7 +224,7 @@ export class TableSorting<T extends TableRow> {
206
224
  // TODO(ZaymonFC): Should we format this returned value for display based on the referenced prop format?
207
225
  // Right now we're going to be sorting based on the raw values.
208
226
  // Maybe we need to recurse this function with the referenced prop format and field?
209
- if (format === FormatEnum.Ref && field.referencePath) {
227
+ if (format === Format.TypeFormat.Ref && field.referencePath) {
210
228
  const refValue = getValue(value.target, field.referencePath);
211
229
  return refValue;
212
230
  }
@@ -53,12 +53,12 @@ export class TableManager {
53
53
  await this.page.getByTestId(TABLE_SELECTORS.columnSort[direction]).click();
54
54
  }
55
55
 
56
- public async addColumn({ label, format }: { label: string; format: 'number' }): Promise<void> {
56
+ public async addColumn({ label, format }: { label: string; format: string }): Promise<void> {
57
57
  await this.page.getByTestId(TABLE_SELECTORS.newColumnButton).click();
58
58
  await this.page.getByRole('combobox').click();
59
- await this.page.getByLabel(`format ${format}`).click();
60
- await this.page.getByPlaceholder('Property label.').click();
61
- await this.page.getByPlaceholder('Property label.').fill(label);
62
- await this.page.getByRole('button', { name: 'button save' }).click();
59
+ await this.page.getByLabel(format).click();
60
+ await this.page.getByPlaceholder('Property label').click();
61
+ await this.page.getByPlaceholder('Property label').fill(label);
62
+ await this.page.getByRole('button', { name: 'Save' }).click();
63
63
  }
64
64
  }
@@ -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,6 +66,7 @@ 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
 
@@ -71,6 +78,7 @@ test.describe('Table', () => {
71
78
 
72
79
  test('delete row--select all', async ({ browser, browserName }) => {
73
80
  test.skip(browserName === 'webkit');
81
+ test.skip(browserName === 'firefox');
74
82
  const { page } = await setupPage(browser, { url: storyUrl });
75
83
  const table = new TableManager(page);
76
84
 
@@ -88,6 +96,7 @@ test.describe('Table', () => {
88
96
 
89
97
  test('delete column', async ({ browser, browserName }) => {
90
98
  test.skip(browserName === 'webkit');
99
+ test.skip(browserName === 'firefox');
91
100
  const { page } = await setupPage(browser, { url: storyUrl });
92
101
  const table = new TableManager(page);
93
102
 
@@ -114,56 +123,22 @@ test.describe('Table', () => {
114
123
  // Rest of add column test remains the same as it's a more complex flow.
115
124
  test('add column', async ({ browser, browserName }) => {
116
125
  test.skip(browserName === 'webkit');
126
+ test.skip(browserName === 'firefox');
117
127
  const { page } = await setupPage(browser, { url: storyUrl });
118
128
  const table = new TableManager(page);
119
129
 
120
130
  await table.grid.ready();
121
131
  const newColumnLabel = 'TEST LABEL';
122
132
 
123
- await table.addColumn({ label: newColumnLabel, format: 'number' });
133
+ await table.addColumn({ label: newColumnLabel, format: 'Number' });
124
134
 
125
135
  await expect(page.getByRole('gridcell', { name: newColumnLabel })).toBeVisible();
126
136
  await page.close();
127
137
  });
128
138
 
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 }) => {
139
+ test.skip('test toggles', async ({ browser, browserName }) => {
166
140
  test.skip(browserName === 'webkit');
141
+ test.skip(browserName === 'firefox');
167
142
  const { page } = await setupPage(browser, { url: storyUrl });
168
143
  const table = new TableManager(page);
169
144
 
@@ -183,4 +158,123 @@ test.describe('Table', () => {
183
158
 
184
159
  await page.close();
185
160
  });
161
+
162
+ // TODO(wittjosiah): Remove? Conflicts with story play function which is run as a unit test anyways.
163
+ test.skip('extant relations work as expected', async ({ browser, browserName }) => {
164
+ test.skip(browserName === 'webkit');
165
+ test.skip(browserName === 'firefox');
166
+ const { page } = await setupPage(browser, { url: relationsStoryUrl });
167
+
168
+ // Wait for the page to load
169
+ await page.locator('dx-grid > .dx-grid').nth(1).waitFor({ state: 'visible' });
170
+
171
+ // Find the dx-grid element for the contactModel (second table)
172
+ // The contactModel is used in the second Table.Main component in the story
173
+ const dxGrid = page.locator('dx-grid').nth(1);
174
+ await dxGrid.waitFor({ state: 'visible' });
175
+
176
+ // Scroll to the last column (column 4)
177
+ await dxGrid.evaluate(async (dxGridElement: DxGrid) => {
178
+ dxGridElement.scrollToColumn(4);
179
+ });
180
+
181
+ // Click on the cell at aria-rowindex=0 aria-colindex=4 to focus it
182
+ const targetCell = dxGrid.locator('[data-dx-grid-plane="grid"] [aria-rowindex="0"][aria-colindex="4"]');
183
+ await targetCell.click();
184
+
185
+ // Click again to engage edit mode
186
+ await page.keyboard.press('Enter');
187
+
188
+ // Click on the combobox to open options
189
+ await page.getByRole('combobox').click();
190
+ await page.pause();
191
+ await page.getByPlaceholder('Search…').focus();
192
+
193
+ // Type the first few letters of an org name.
194
+ const orgName =
195
+ (await page
196
+ .locator('dx-grid')
197
+ .nth(0)
198
+ .locator('[data-dx-grid-plane="grid"] [aria-rowindex="0"][aria-colindex="0"] .dx-grid__cell__content')
199
+ .textContent()) ?? 'never';
200
+ await page.keyboard.type(orgName.substring(0, 4), { delay: 500 });
201
+
202
+ // Assert that there is an element with aria-selected on the page
203
+ await expect(page.locator('[role="option"][aria-selected]')).toBeVisible();
204
+
205
+ // Type the enter key
206
+ await page.keyboard.press('Enter');
207
+
208
+ // Save the result
209
+ await page.getByTestId('save-button').click();
210
+
211
+ // Assert that the cell element has the org name
212
+ await expect(targetCell).toHaveText(orgName);
213
+
214
+ await page.close();
215
+ });
216
+
217
+ // TODO(wittjosiah): Remove? Conflicts with story play function which is run as a unit test anyways.
218
+ test.skip('new relations work as expected', async ({ browser, browserName }) => {
219
+ test.skip(browserName === 'webkit');
220
+ test.skip(browserName === 'firefox');
221
+ const { page } = await setupPage(browser, { url: relationsStoryUrl });
222
+
223
+ // Wait for the page to load
224
+ await page.locator('dx-grid > .dx-grid').nth(1).waitFor({ state: 'visible' });
225
+
226
+ // Find the dx-grid element for the contactModel (second table)
227
+ // The contactModel is used in the second Table.Main component in the story
228
+ const dxGrid = page.locator('dx-grid').nth(1);
229
+ await dxGrid.waitFor({ state: 'visible' });
230
+
231
+ // Scroll to the last column (column 4)
232
+ await dxGrid.evaluate(async (dxGridElement: DxGrid) => {
233
+ dxGridElement.scrollToColumn(4);
234
+ });
235
+
236
+ // Click on the cell at aria-rowindex=0 aria-colindex=8 to focus it
237
+ const targetCell = dxGrid.locator('[data-dx-grid-plane="grid"] [aria-rowindex="0"][aria-colindex="4"]');
238
+ await targetCell.click();
239
+
240
+ // Click again to engage edit mode
241
+ await page.keyboard.press('Enter');
242
+
243
+ // Click on the combobox to open options
244
+ await page.getByRole('combobox').click();
245
+ await page.getByPlaceholder('Search…').focus();
246
+
247
+ // Type the first few letters of an org name.
248
+ const orgName = 'Sally';
249
+ await page.keyboard.type(orgName, { delay: 500 });
250
+
251
+ // Assert that there is an element with aria-selected on the page
252
+ await expect(page.locator('[role="option"][aria-selected]')).toBeVisible();
253
+
254
+ // Type the enter key
255
+ await page.keyboard.press('Enter');
256
+ await page.pause();
257
+
258
+ // Click the save button in the popover
259
+ await page.getByTestId('create-referenced-object-form').getByTestId('save-button').click();
260
+
261
+ // Assert that the cell element has the org name
262
+ await expect(targetCell).toHaveText(orgName);
263
+
264
+ // Make a change to a non-ref cell to check that populated refs don’t cause problems with snapshots or schemas.
265
+ await dxGrid.evaluate(async (dxGridElement: DxGrid) => {
266
+ dxGridElement.scrollToColumn(6);
267
+ });
268
+
269
+ const nonRefContent = faker.lorem.words(3);
270
+ const nonRefCell = dxGrid.locator('[data-dx-grid-plane="grid"] [aria-rowindex="0"][aria-colindex="6"]');
271
+ await nonRefCell.click();
272
+ await page.keyboard.press('Enter');
273
+ await page.getByTestId('grid.cell-editor').waitFor({ state: 'visible' });
274
+ await page.keyboard.type(nonRefContent, { delay: 500 });
275
+ await page.keyboard.press('Enter');
276
+ await expect(nonRefCell).toHaveText(nonRefContent);
277
+
278
+ await page.close();
279
+ });
186
280
  });